strapi-plugin-comments 2.0.0-rc.1 → 2.0.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 +109 -67
- package/__mocks__/initSetup.js +53 -0
- package/admin/src/components/ActionButton/styles.js +10 -0
- package/admin/src/components/DiscussionThread/index.js +3 -7
- package/admin/src/components/DiscussionThreadItem/index.js +25 -70
- package/admin/src/components/DiscussionThreadItem/styles.js +4 -79
- package/admin/src/components/DiscussionThreadItemActions/index.js +82 -40
- package/admin/src/components/DiscussionThreadItemActions/styles.js +1 -12
- package/admin/src/components/DiscussionThreadItemApprovalFlowActions/index.js +2 -3
- package/admin/src/components/DiscussionThreadItemFooter/index.js +47 -0
- package/admin/src/components/DiscussionThreadItemFooter/styles.js +13 -0
- package/admin/src/components/DiscussionThreadItemReviewAction/index.js +131 -0
- package/admin/src/components/IconButton/styles.js +14 -0
- package/admin/src/components/ReportsReviewModal/index.js +1 -1
- package/admin/src/components/ReportsReviewTable/index.js +12 -6
- package/admin/src/components/StatusBadge/index.js +17 -0
- package/admin/src/components/StatusBadge/styles.js +17 -0
- package/admin/src/pages/Details/components/DetailsEntity/index.js +42 -24
- package/admin/src/pages/Details/components/DetailsFilters/index.js +8 -10
- package/admin/src/pages/Details/index.js +6 -15
- package/admin/src/pages/Discover/components/DiscoverTableRow/index.js +30 -67
- package/admin/src/translations/en.json +13 -15
- package/admin/src/utils/resolveCommentStatus.js +4 -4
- package/content-types/comment.js +0 -4
- package/package.json +8 -5
- package/server/controllers/client.js +11 -6
- package/server/controllers/utils/__tests__/functions.test.js +2 -2
- package/server/controllers/utils/parsers.js +6 -2
- package/server/graphql/mutations/createAbuseReport.js +8 -3
- package/server/graphql/mutations/createComment.js +8 -3
- package/server/graphql/mutations/removeComment.js +8 -3
- package/server/graphql/mutations/updateComment.js +8 -3
- package/server/graphql/queries/findAllFlat.js +4 -2
- package/server/graphql/queries/findAllInHierarchy.js +3 -3
- package/server/graphql/resolvers-config.js +13 -8
- package/server/graphql/types/comment-nested.js +1 -1
- package/server/graphql/types/comment.js +0 -1
- package/server/graphql/types/create-comment-author.js +1 -1
- package/server/graphql/types/identify-comment-author.js +1 -1
- package/server/graphql/types/index.js +0 -1
- package/server/services/__tests__/common.test.js +165 -0
- package/server/services/admin.js +10 -7
- package/server/services/client.js +45 -31
- package/server/services/common.js +71 -19
- package/server/services/utils/__tests__/functions.test.js +9 -73
- package/server/services/utils/functions.js +23 -34
- package/server/utils/__tests__/functions.test.js +17 -0
- package/server/utils/constants.js +1 -0
- package/.circleci/config.yml +0 -48
- package/.eslintrc +0 -35
- package/.github/pull_request_template.md +0 -13
- package/.nvmrc +0 -1
- package/admin/src/components/DiscussionThreadItemMeta/index.js +0 -42
- package/admin/src/components/DiscussionThreadItemWarnings/index.js +0 -147
- package/admin/src/components/DiscussionThreadItemWarnings/styles.js +0 -7
- package/codecov.yml +0 -4
- package/public/assets/logo.png +0 -0
- package/public/assets/preview.png +0 -0
- package/server/graphql/types/comment-author-user.js +0 -9
|
@@ -1,88 +1,13 @@
|
|
|
1
1
|
import styled from 'styled-components';
|
|
2
2
|
import { Box } from '@strapi/design-system/Box';
|
|
3
|
-
import { Flex } from '@strapi/design-system/Flex';
|
|
4
3
|
|
|
5
|
-
export const
|
|
6
|
-
export const InitialsWrapper = styled(Box)``;
|
|
7
|
-
|
|
8
|
-
export const DiscussionThreadItemLinkBox = styled(Flex)`
|
|
4
|
+
export const DiscussionThreadFullsize = styled(Box)`
|
|
9
5
|
width: 100%;
|
|
10
|
-
margin-top: .5rem;
|
|
11
|
-
margin-bottom: .5rem;
|
|
12
6
|
`
|
|
13
|
-
|
|
14
|
-
const DiscussionThreadItemStyled = styled(Box)`
|
|
15
|
-
display: flex;
|
|
16
|
-
flex-direction: column;
|
|
17
|
-
width: 100%;
|
|
18
|
-
margin-bottom: 1rem;
|
|
19
|
-
|
|
20
|
-
${AvatarWrapper},
|
|
21
|
-
${InitialsWrapper} {
|
|
22
|
-
align-self: flex-start;
|
|
23
|
-
margin-top: .4rem;
|
|
24
|
-
}
|
|
25
|
-
`;
|
|
26
|
-
|
|
27
|
-
export const DiscussionThreadItemContent = styled(Box)`
|
|
28
|
-
max-width: 45%;
|
|
29
|
-
`;
|
|
30
|
-
|
|
31
|
-
export const DiscussionThreadItemLeft = styled(DiscussionThreadItemStyled)`
|
|
7
|
+
export const DiscussionThreadItemContent = styled(DiscussionThreadFullsize)`
|
|
32
8
|
align-items: flex-start;
|
|
33
|
-
padding-left: 4rem;
|
|
34
|
-
text-align: left;
|
|
35
|
-
|
|
36
|
-
${DiscussionThreadItemContent} {
|
|
37
|
-
align-self: flex-start;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
${AvatarWrapper},
|
|
41
|
-
${InitialsWrapper} {
|
|
42
|
-
margin-right: 1rem;
|
|
43
|
-
}
|
|
44
9
|
`;
|
|
45
10
|
|
|
46
|
-
export const
|
|
47
|
-
align-items: flex-
|
|
48
|
-
padding-right: 4rem;
|
|
49
|
-
text-align: left;
|
|
50
|
-
|
|
51
|
-
${DiscussionThreadItemContent} {
|
|
52
|
-
align-self: flex-end;
|
|
53
|
-
flex-direction: row-reverse;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
${AvatarWrapper},
|
|
57
|
-
${InitialsWrapper} {
|
|
58
|
-
margin-left: 1rem;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
${DiscussionThreadItemLinkBox} {
|
|
62
|
-
margin-right: 3rem;
|
|
63
|
-
}
|
|
64
|
-
`;
|
|
65
|
-
|
|
66
|
-
export const DiscussionThreadItemBox = styled(Box)`
|
|
67
|
-
|
|
11
|
+
export const DiscussionThreadItemContainer = styled(DiscussionThreadFullsize)`
|
|
12
|
+
align-items: flex-start;
|
|
68
13
|
`;
|
|
69
|
-
|
|
70
|
-
export const DiscussionThreadItemBoxContent = styled(Box)`
|
|
71
|
-
width: 100%;
|
|
72
|
-
align-items: stretch;
|
|
73
|
-
`
|
|
74
|
-
|
|
75
|
-
export const DiscussionThreadItemRoot = styled(DiscussionThreadItemLeft)`
|
|
76
|
-
padding-left: 0;
|
|
77
|
-
padding-right: 0;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
${DiscussionThreadItemContent} {
|
|
81
|
-
width: 100%;
|
|
82
|
-
max-width: 100%;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
${DiscussionThreadItemBox} {
|
|
86
|
-
flex-grow: 1;
|
|
87
|
-
}
|
|
88
|
-
`;
|
|
@@ -7,24 +7,42 @@
|
|
|
7
7
|
import React, { useState } from 'react';
|
|
8
8
|
import PropTypes from 'prop-types';
|
|
9
9
|
import { useMutation, useQueryClient } from 'react-query';
|
|
10
|
+
import { useHistory } from 'react-router-dom';
|
|
10
11
|
import { isNil, isEmpty } from 'lodash';
|
|
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
14
|
import { useNotification, useOverlayBlocker } from '@strapi/helper-plugin';
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
15
|
+
import { Eye } from '@strapi/icons';
|
|
16
|
+
import { getMessage, getUrl, handleAPIError, resolveCommentStatus, resolveCommentStatusColor } from '../../utils';
|
|
17
|
+
import { DiscussionThreadItemActionsWrapper } from './styles';
|
|
17
18
|
import ConfirmationDialog from '../ConfirmationDialog';
|
|
18
19
|
import { blockItem, blockItemThread, unblockItem, unblockItemThread } from '../../pages/utils/api';
|
|
19
20
|
import pluginId from '../../pluginId';
|
|
20
21
|
import { LockIcon, UnlockIcon } from '../icons';
|
|
21
22
|
import DiscussionThreadItemApprovalFlowActions from '../DiscussionThreadItemApprovalFlowActions';
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
import StatusBadge from '../StatusBadge';
|
|
24
|
+
import { IconButtonGroupStyled } from '../IconButton/styles';
|
|
25
|
+
import { ActionButton } from '../ActionButton/styles';
|
|
26
|
+
import DiscussionThreadItemReviewAction from '../DiscussionThreadItemReviewAction';
|
|
27
|
+
|
|
28
|
+
const DiscussionThreadItemActions = ({ allowedActions: { canModerate, canAccessReports, canReviewReports }, ...item }) => {
|
|
29
|
+
const { id,
|
|
30
|
+
blocked,
|
|
31
|
+
removed,
|
|
32
|
+
blockedThread,
|
|
33
|
+
gotThread,
|
|
34
|
+
threadItemsCount,
|
|
35
|
+
threadFirstItemId,
|
|
36
|
+
pinned,
|
|
37
|
+
preview,
|
|
38
|
+
reports = [],
|
|
39
|
+
approvalStatus } = item;
|
|
24
40
|
|
|
25
41
|
const [blockConfirmationVisible, setBlockConfirmationVisible] = useState(false);
|
|
26
42
|
const [blockThreadConfirmationVisible, setBlockThreadConfirmationVisible] = useState(false);
|
|
43
|
+
const [blockButtonsDisabled, setBlockButtonsDisabled] = useState(blockedThread);
|
|
27
44
|
|
|
45
|
+
const { push } = useHistory();
|
|
28
46
|
const toggleNotification = useNotification();
|
|
29
47
|
const queryClient = useQueryClient();
|
|
30
48
|
const { lockApp, unlockApp } = useOverlayBlocker();
|
|
@@ -75,17 +93,20 @@ const DiscussionThreadItemActions = ({ id, blocked, removed, blockedThread, gotT
|
|
|
75
93
|
const openReports = reports?.filter(_ => !_.resolved);
|
|
76
94
|
const hasReports = !isEmpty(openReports);
|
|
77
95
|
const reviewFlowEnabled = (canAccessReports || canReviewReports) && hasReports;
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
96
|
+
const hasActiveThread = gotThread && !(removed || preview || pinned || blockedThread);
|
|
97
|
+
const isStatusBadgeVisible = isBlocked || reviewFlowEnabled;
|
|
98
|
+
|
|
99
|
+
const renderStatus = (props) => {
|
|
100
|
+
const status = resolveCommentStatus({ ...props, reviewFlowEnabled });
|
|
101
|
+
const color = resolveCommentStatusColor(status);
|
|
102
|
+
|
|
103
|
+
return (<StatusBadge backgroundColor={`${color}100`} textColor={`${color}700`} color={color}>{ getMessage({
|
|
104
|
+
id: `page.common.item.status.${status}`,
|
|
105
|
+
props: {
|
|
106
|
+
count: openReports.length
|
|
107
|
+
}
|
|
108
|
+
}, status) }</StatusBadge>);
|
|
109
|
+
};
|
|
89
110
|
|
|
90
111
|
const handleBlockClick = () => setBlockConfirmationVisible(true);
|
|
91
112
|
const handleBlockConfirm = async () => {
|
|
@@ -121,36 +142,57 @@ const DiscussionThreadItemActions = ({ id, blocked, removed, blockedThread, gotT
|
|
|
121
142
|
}
|
|
122
143
|
};
|
|
123
144
|
|
|
145
|
+
const handleDrilldownClick = () => {
|
|
146
|
+
if (hasActiveThread) {
|
|
147
|
+
push(getUrl(`discover/${threadFirstItemId}`));
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const handleBlockActionClick = async (mutation, onCallback) => {
|
|
152
|
+
lockApp();
|
|
153
|
+
await mutation.mutateAsync(id);
|
|
154
|
+
onCallback();
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const handleBlockButtonsStateChange = disabled => setBlockButtonsDisabled(disabled);
|
|
158
|
+
|
|
159
|
+
const anyGroupButtonsVisible = needsApproval || reviewFlowEnabled || !blockedThread;
|
|
160
|
+
const isLoading = unblockItemMutation.isLoading || blockItemMutation.isLoading || blockItemThreadMutation.isLoading || unblockItemThreadMutation.isLoading;
|
|
161
|
+
|
|
124
162
|
if (removed || isRejected || !canModerate) {
|
|
125
163
|
return (<DiscussionThreadItemActionsWrapper as={Flex} direction="row">
|
|
126
|
-
|
|
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>
|
|
164
|
+
{ renderStatus(item) }
|
|
135
165
|
</DiscussionThreadItemActionsWrapper>);
|
|
136
166
|
}
|
|
137
167
|
|
|
138
168
|
return (<>
|
|
139
169
|
<DiscussionThreadItemActionsWrapper as={Flex} direction="row">
|
|
140
|
-
{
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
170
|
+
{ isStatusBadgeVisible && (renderStatus(item))}
|
|
171
|
+
{ (!blockedThread && (gotThread || pinned)) && (<ActionButton onClick={handleBlockThreadClick} startIcon={<LockIcon />} loading={blockItemThreadMutation.isLoading} variant="danger">{ getMessage('page.details.actions.thread.block', 'Block thread') }</ActionButton>) }
|
|
172
|
+
{ (blockedThread && (gotThread || pinned)) && (<ActionButton onClick={handleUnblockThreadClick} startIcon={<UnlockIcon />} loading={unblockItemThreadMutation.isLoading} variant="success">{ getMessage('page.details.actions.thread.unblock', 'Unblock thread') }</ActionButton>) }
|
|
173
|
+
{ anyGroupButtonsVisible && (<IconButtonGroupStyled isSingle withMargin>
|
|
174
|
+
{ !blockedThread && !(blocked || needsApproval) && (<IconButton onClick={handleBlockClick} loading={blockItemMutation.isLoading} icon={<LockIcon />} label={getMessage('page.details.actions.comment.block', 'Block')} />)}
|
|
175
|
+
{ !blockedThread && blocked && (<IconButton onClick={handleUnblockClick} loading={unblockItemMutation.isLoading} icon={<UnlockIcon />} label={getMessage('page.details.actions.comment.unblock', 'Unblock')} />)}
|
|
176
|
+
{ needsApproval && (<DiscussionThreadItemApprovalFlowActions id={id} allowedActions={{ canModerate }} queryToInvalidate="get-details-data" />) }
|
|
177
|
+
<DiscussionThreadItemReviewAction
|
|
178
|
+
item={item}
|
|
179
|
+
queryToInvalidate="get-details-data"
|
|
180
|
+
areBlockButtonsDisabled={blockButtonsDisabled}
|
|
181
|
+
isLoading={isLoading}
|
|
182
|
+
allowedActions={{ canModerate, canAccessReports, canReviewReports }}
|
|
183
|
+
blockItemMutation={blockItemMutation}
|
|
184
|
+
blockItemThreadMutation={blockItemThreadMutation}
|
|
185
|
+
onBlockButtonsStateChange={handleBlockButtonsStateChange}
|
|
186
|
+
onBlockActionClick={handleBlockActionClick}
|
|
187
|
+
/>
|
|
188
|
+
</IconButtonGroupStyled>)}
|
|
189
|
+
{ hasActiveThread && (<IconButtonGroupStyled isSingle withMargin>
|
|
190
|
+
<IconButton
|
|
191
|
+
onClick={handleDrilldownClick}
|
|
192
|
+
icon={<Eye />}
|
|
193
|
+
label={getMessage('page.details.panel.discussion.nav.drilldown', 'Drilldown thread')}
|
|
194
|
+
style={(blocked && !blockedThread) ? { marginTop: '1px', marginRight: '.5rem' } : {}} />
|
|
195
|
+
</IconButtonGroupStyled>) }
|
|
154
196
|
</DiscussionThreadItemActionsWrapper>
|
|
155
197
|
{ !blocked && (<ConfirmationDialog
|
|
156
198
|
isVisible={blockConfirmationVisible}
|
|
@@ -162,7 +204,7 @@ const DiscussionThreadItemActions = ({ id, blocked, removed, blockedThread, gotT
|
|
|
162
204
|
onCancel={handleBlockCancel}>
|
|
163
205
|
{ getMessage('page.details.actions.comment.block.confirmation.description') }
|
|
164
206
|
</ConfirmationDialog>) }
|
|
165
|
-
{
|
|
207
|
+
{ !blockedThread && (<ConfirmationDialog
|
|
166
208
|
isVisible={blockThreadConfirmationVisible}
|
|
167
209
|
isActionAsync={blockItemThreadMutation.isLoading}
|
|
168
210
|
header={getMessage('page.details.actions.thread.block.confirmation.header')}
|
|
@@ -1,18 +1,7 @@
|
|
|
1
1
|
import styled from 'styled-components';
|
|
2
2
|
import { Box } from '@strapi/design-system/Box';
|
|
3
|
-
import { Badge } from '@strapi/design-system/Badge';
|
|
4
|
-
import { IconButtonGroup } from '@strapi/design-system/IconButton';
|
|
5
|
-
|
|
6
|
-
export const DiscussionThreadItemActionsBadge = styled(Badge)`
|
|
7
|
-
border: 1px ${ ({ theme, color }) => theme.colors[`${color}200`] } solid;
|
|
8
|
-
`;
|
|
9
3
|
|
|
10
4
|
export const DiscussionThreadItemActionsWrapper = styled(Box)`
|
|
11
5
|
margin-left: 1rem;
|
|
12
|
-
align-items: start;
|
|
13
|
-
|
|
14
|
-
${DiscussionThreadItemActionsBadge} {
|
|
15
|
-
margin-top: .175rem;
|
|
16
|
-
margin-right: .5rem;
|
|
17
|
-
}
|
|
6
|
+
align-items: flex-start;
|
|
18
7
|
`;
|
|
@@ -11,7 +11,6 @@ import { IconButton } from '@strapi/design-system/IconButton';
|
|
|
11
11
|
import { Check, Cross } from '@strapi/icons';
|
|
12
12
|
import { useNotification, useOverlayBlocker } from '@strapi/helper-plugin';
|
|
13
13
|
import { getMessage, handleAPIError } from '../../utils';
|
|
14
|
-
import { DiscussionThreadItemActionsGroupWrapper } from './styles';
|
|
15
14
|
import { approveItem, rejectItem } from '../../pages/utils/api';
|
|
16
15
|
import pluginId from '../../pluginId';
|
|
17
16
|
|
|
@@ -60,10 +59,10 @@ const DiscussionThreadItemApprovalFlowActions = ({ id, allowedActions: { canMode
|
|
|
60
59
|
};
|
|
61
60
|
|
|
62
61
|
if (canModerate) {
|
|
63
|
-
return (
|
|
62
|
+
return (<>
|
|
64
63
|
<IconButton icon={<Check />} label={getMessage('page.details.actions.comment.approve', 'Approve')} onClick={handleApproveClick} />
|
|
65
64
|
<IconButton icon={<Cross />} label={getMessage('page.details.actions.comment.reject', 'Reject')} onClick={handleRejectClick} />
|
|
66
|
-
|
|
65
|
+
</>);
|
|
67
66
|
}
|
|
68
67
|
return null;
|
|
69
68
|
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Discussion thread - Item footer
|
|
4
|
+
*
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import PropTypes from 'prop-types';
|
|
9
|
+
import { useIntl } from 'react-intl';
|
|
10
|
+
import { Avatar, Initials } from '@strapi/design-system/Avatar';
|
|
11
|
+
import { Box } from '@strapi/design-system/Box';
|
|
12
|
+
import { Typography } from '@strapi/design-system/Typography';
|
|
13
|
+
import { renderInitials } from '../../utils';
|
|
14
|
+
import { DiscussionThreadItemFooterMeta, DiscussionThreadItemFooterStyled } from './styles';
|
|
15
|
+
|
|
16
|
+
const DiscussionThreadItemFooter = ({ children, author, createdAt, updatedAt }) => {
|
|
17
|
+
|
|
18
|
+
const { formatDate } = useIntl();
|
|
19
|
+
|
|
20
|
+
const dateTime = formatDate(updatedAt || createdAt, { dateStyle: 'medium', timeStyle: 'short' });
|
|
21
|
+
const { name, avatar } = author;
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
return (<DiscussionThreadItemFooterStyled as={Box} paddingTop={2} direction="row">
|
|
25
|
+
<DiscussionThreadItemFooterMeta size={3} horizontal>
|
|
26
|
+
{ avatar ? <Avatar src={avatar} alt={name} /> : <Initials>{ renderInitials(name) }</Initials> }
|
|
27
|
+
<Typography variant="pi" fontWeight="bold" textColor="neutral800">{ name }</Typography>
|
|
28
|
+
<Typography variant="pi" textColor="neutral600">{ dateTime }</Typography>
|
|
29
|
+
</DiscussionThreadItemFooterMeta>
|
|
30
|
+
{ children }
|
|
31
|
+
</DiscussionThreadItemFooterStyled>);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
DiscussionThreadItemFooter.propTypes = {
|
|
35
|
+
author: PropTypes.shape({
|
|
36
|
+
id: PropTypes.oneOfType(PropTypes.string, PropTypes.number).isRequired,
|
|
37
|
+
name: PropTypes.string.isRequired,
|
|
38
|
+
email: PropTypes.string,
|
|
39
|
+
avatar: PropTypes.string,
|
|
40
|
+
}).isRequired,
|
|
41
|
+
createdAt: PropTypes.string.isRequired,
|
|
42
|
+
updatedAt: PropTypes.string,
|
|
43
|
+
children: PropTypes.array,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export default DiscussionThreadItemFooter;
|
|
47
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import styled from 'styled-components';
|
|
2
|
+
import { Flex } from '@strapi/design-system/Flex';
|
|
3
|
+
import { Stack } from '@strapi/design-system/Stack';
|
|
4
|
+
|
|
5
|
+
export const DiscussionThreadItemFooterStyled = styled(Flex)`
|
|
6
|
+
width: 100%;
|
|
7
|
+
`;
|
|
8
|
+
|
|
9
|
+
export const DiscussionThreadItemFooterMeta = styled(Stack)`
|
|
10
|
+
* + * {
|
|
11
|
+
margin-left: 8px;
|
|
12
|
+
}
|
|
13
|
+
`;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { useMutation, useQueryClient } from 'react-query';
|
|
4
|
+
import { isNil, isEmpty, orderBy } from 'lodash';
|
|
5
|
+
import { Button } from '@strapi/design-system/Button';
|
|
6
|
+
import { IconButton } from '@strapi/design-system/IconButton';
|
|
7
|
+
import { useNotification, useOverlayBlocker } from '@strapi/helper-plugin';
|
|
8
|
+
import { ReviewIcon, LockIcon } from '../icons';
|
|
9
|
+
import { resolveReport } from '../../pages/utils/api';
|
|
10
|
+
import { getMessage, handleAPIError } from '../../utils';
|
|
11
|
+
import ReportsReviewTable from '../ReportsReviewTable';
|
|
12
|
+
import ReportsReviewModal from '../ReportsReviewModal';
|
|
13
|
+
import pluginId from '../../pluginId';
|
|
14
|
+
|
|
15
|
+
const DiscussionThreadItemReviewAction = ({
|
|
16
|
+
item,
|
|
17
|
+
isAnyActionLoading: isAnyActionLoading,
|
|
18
|
+
queryToInvalidate,
|
|
19
|
+
areBlockButtonsDisabled,
|
|
20
|
+
allowedActions: { canModerate, canAccessReports, canReviewReports },
|
|
21
|
+
blockItemMutation,
|
|
22
|
+
blockItemThreadMutation,
|
|
23
|
+
onBlockButtonsStateChange,
|
|
24
|
+
onBlockActionClick }) => {
|
|
25
|
+
|
|
26
|
+
const { reports } = item;
|
|
27
|
+
|
|
28
|
+
const [reportsReviewVisible, setReportsReviewVisible] = useState(false);
|
|
29
|
+
|
|
30
|
+
const toggleNotification = useNotification();
|
|
31
|
+
const queryClient = useQueryClient();
|
|
32
|
+
const { unlockApp } = useOverlayBlocker();
|
|
33
|
+
|
|
34
|
+
const onSuccess = (message, stateAction = () => {}, indalidate = true) => async () => {
|
|
35
|
+
if (indalidate) {
|
|
36
|
+
await queryClient.invalidateQueries(queryToInvalidate);
|
|
37
|
+
}
|
|
38
|
+
toggleNotification({
|
|
39
|
+
type: 'success',
|
|
40
|
+
message: `${pluginId}.${message}`,
|
|
41
|
+
});
|
|
42
|
+
stateAction(false);
|
|
43
|
+
unlockApp();
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const onError = err => { handleAPIError(err, toggleNotification); };
|
|
47
|
+
|
|
48
|
+
const resolveReportMutation = useMutation(resolveReport, {
|
|
49
|
+
onSuccess: onSuccess('page.details.panel.discussion.warnings.reports.dialog.confirmation.success', () => {}, false),
|
|
50
|
+
onError,
|
|
51
|
+
refetchActive: false,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const handleBlockActionClick = () => setReportsReviewVisible(false);
|
|
55
|
+
|
|
56
|
+
const handleReportsReviewClick = () => {
|
|
57
|
+
if (canAccessReports) {
|
|
58
|
+
setReportsReviewVisible(true);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const handleBlockItemClick = async () => {
|
|
62
|
+
if (canModerate) {
|
|
63
|
+
await onBlockActionClick(blockItemMutation, handleBlockActionClick);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const handleBlockItemThreadClick = async () => {
|
|
67
|
+
if (canModerate) {
|
|
68
|
+
await onBlockActionClick(blockItemThreadMutation, handleBlockActionClick);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const handleReportsReviewClose = async () => {
|
|
73
|
+
await queryClient.invalidateQueries(queryToInvalidate);
|
|
74
|
+
setReportsReviewVisible(false);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const isLoading = isAnyActionLoading && resolveReportMutation.isLoading;
|
|
78
|
+
const openReports = reports?.filter(_ => !_.resolved);
|
|
79
|
+
const hasReports = !isEmpty(openReports);
|
|
80
|
+
const reviewFlowEnabled = (canAccessReports && hasReports) && !(item.blocked || item.blockedThread);
|
|
81
|
+
|
|
82
|
+
if (reviewFlowEnabled) {
|
|
83
|
+
return (<>
|
|
84
|
+
<IconButton onClick={handleReportsReviewClick} label={ getMessage('page.discover.table.reports.review') } icon={<ReviewIcon />} />
|
|
85
|
+
<ReportsReviewModal
|
|
86
|
+
isVisible={reportsReviewVisible}
|
|
87
|
+
isActionAsync={isLoading}
|
|
88
|
+
allowedActions={{ canModerate, canAccessReports, canReviewReports }}
|
|
89
|
+
onClose={handleReportsReviewClose}
|
|
90
|
+
startActions={<Button onClick={handleReportsReviewClose} variant="tertiary">
|
|
91
|
+
{ getMessage('compontents.confirmation.dialog.button.cancel', 'Cancel') }
|
|
92
|
+
</Button>}
|
|
93
|
+
endActions={canModerate && (<>
|
|
94
|
+
<Button onClick={handleBlockItemClick} variant="danger-light" startIcon={<LockIcon />} disabled={areBlockButtonsDisabled}>
|
|
95
|
+
{ getMessage(`page.details.actions.comment.block`, 'Block comment') }
|
|
96
|
+
</Button>
|
|
97
|
+
{ item.gotThread && (<Button onClick={handleBlockItemThreadClick} variant="danger" startIcon={<LockIcon />} disabled={areBlockButtonsDisabled}>
|
|
98
|
+
{ getMessage(`page.details.actions.thread.block`, 'Block thread') }
|
|
99
|
+
</Button>) }
|
|
100
|
+
</>)}
|
|
101
|
+
item={item}>
|
|
102
|
+
<ReportsReviewTable
|
|
103
|
+
commentId={item.id}
|
|
104
|
+
items={orderBy(reports, ['resolved', 'createdAt'], ['DESC', 'DESC'])}
|
|
105
|
+
mutation={resolveReportMutation}
|
|
106
|
+
allowedActions={{ canAccessReports, canReviewReports }}
|
|
107
|
+
onBlockButtonsStateChange={onBlockButtonsStateChange}
|
|
108
|
+
/>
|
|
109
|
+
</ReportsReviewModal>
|
|
110
|
+
</>);
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
DiscussionThreadItemReviewAction.propTypes = {
|
|
116
|
+
item: PropTypes.object.isRequired,
|
|
117
|
+
isLoading: PropTypes.bool,
|
|
118
|
+
queryToInvalidate: PropTypes.string.isRequired,
|
|
119
|
+
areBlockButtonsDisabled: PropTypes.bool,
|
|
120
|
+
allowedActions: PropTypes.shape({
|
|
121
|
+
canModerate: PropTypes.bool,
|
|
122
|
+
canAccessReports: PropTypes.bool,
|
|
123
|
+
canReviewReports: PropTypes.bool,
|
|
124
|
+
}),
|
|
125
|
+
blockItemMutation: PropTypes.func.isRequired,
|
|
126
|
+
blockItemThreadMutation: PropTypes.func.isRequired,
|
|
127
|
+
onBlockButtonsStateChange: PropTypes.func.isRequired,
|
|
128
|
+
onBlockActionClick: PropTypes.func.isRequired,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export default DiscussionThreadItemReviewAction;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
import { IconButtonGroup } from '@strapi/design-system/IconButton';
|
|
4
|
+
|
|
5
|
+
export const IconButtonGroupStyled = styled(IconButtonGroup)`
|
|
6
|
+
${ ({ isSingle }) => isSingle && (`
|
|
7
|
+
span:first-child button {
|
|
8
|
+
border-radius: 4px;
|
|
9
|
+
}
|
|
10
|
+
`)}
|
|
11
|
+
${ ({ withMargin }) => withMargin && (`
|
|
12
|
+
margin-left: .5rem;
|
|
13
|
+
`)}
|
|
14
|
+
`;
|
|
@@ -24,7 +24,7 @@ const ReportsReviewModal = ({
|
|
|
24
24
|
<Typography fontWeight="bold" textColor="neutral800" as="h2" id="title">{ getMessage('page.details.panel.discussion.warnings.reports.dialog.header', 'Open reports') }</Typography>
|
|
25
25
|
</ModalHeader>
|
|
26
26
|
<ModalBody>
|
|
27
|
-
<DiscussionThreadItem {...item} root preview isSelected={
|
|
27
|
+
<DiscussionThreadItem as="div" {...item} root preview isSelected={true} />
|
|
28
28
|
{ children }
|
|
29
29
|
</ModalBody>
|
|
30
30
|
<ModalFooter variant="tertiary" startActions={startActions} endActions={endActions} />
|
|
@@ -8,8 +8,6 @@
|
|
|
8
8
|
import { useIntl } from 'react-intl';
|
|
9
9
|
import PropTypes from 'prop-types';
|
|
10
10
|
import { isEmpty } from 'lodash';
|
|
11
|
-
import { Badge } from '@strapi/design-system/Badge';
|
|
12
|
-
import { Button } from '@strapi/design-system/Button';
|
|
13
11
|
import { BaseCheckbox } from '@strapi/design-system/BaseCheckbox';
|
|
14
12
|
import { Flex } from '@strapi/design-system/Flex';
|
|
15
13
|
import { Table, Thead, Tbody, TFooter, Tr, Th, Td } from '@strapi/design-system/Table';
|
|
@@ -19,6 +17,8 @@
|
|
|
19
17
|
import { useOverlayBlocker } from '@strapi/helper-plugin';
|
|
20
18
|
import { getMessage } from '../../utils';
|
|
21
19
|
import { REPORT_REASON, REPORT_STATUS } from '../../utils/constants';
|
|
20
|
+
import StatusBadge from '../StatusBadge';
|
|
21
|
+
import { ActionButton } from '../ActionButton/styles';
|
|
22
22
|
|
|
23
23
|
const ReportsReviewTable = ({
|
|
24
24
|
commentId,
|
|
@@ -43,7 +43,7 @@ const ReportsReviewTable = ({
|
|
|
43
43
|
case REPORT_STATUS.OPEN:
|
|
44
44
|
color = 'danger';
|
|
45
45
|
};
|
|
46
|
-
return (<
|
|
46
|
+
return (<StatusBadge backgroundColor={`${color}100`} textColor={`${color}600`} color={color}>{ getMessage(`page.details.panel.discussion.warnings.reports.dialog.status.${status}`, status) }</StatusBadge>);
|
|
47
47
|
};
|
|
48
48
|
|
|
49
49
|
const renderReason = reason => {
|
|
@@ -55,7 +55,7 @@ const ReportsReviewTable = ({
|
|
|
55
55
|
case REPORT_REASON.BAD_LANGUAGE:
|
|
56
56
|
color = 'warning';
|
|
57
57
|
};
|
|
58
|
-
return (<
|
|
58
|
+
return (<StatusBadge backgroundColor={`${color}100`} textColor={`${color}600`} color={color}>{ getMessage(`page.details.panel.discussion.warnings.reports.dialog.reason.${reason}`, reason) }</StatusBadge>);
|
|
59
59
|
};
|
|
60
60
|
|
|
61
61
|
const handleClickResolve = async (reportId) => {
|
|
@@ -125,11 +125,17 @@ const ReportsReviewTable = ({
|
|
|
125
125
|
<Typography textColor="neutral800">{ formatDate(entry.createdAt, { dateStyle: 'short', timeStyle: 'short' }) }</Typography>
|
|
126
126
|
</Td>
|
|
127
127
|
<Td>
|
|
128
|
-
|
|
128
|
+
{ renderStatus(entry.resolved) }
|
|
129
129
|
</Td>
|
|
130
130
|
{ canReviewReports && (<Td>
|
|
131
131
|
{ !entry.resolved && (<Flex direction="column" alignItems="flex-end">
|
|
132
|
-
<
|
|
132
|
+
<ActionButton
|
|
133
|
+
isSingle
|
|
134
|
+
onClick={() => handleClickResolve(entry.id)}
|
|
135
|
+
startIcon={<Check />}
|
|
136
|
+
variant="success">
|
|
137
|
+
{ getMessage('page.details.panel.discussion.warnings.reports.dialog.actions.resolve', 'resolve') }
|
|
138
|
+
</ActionButton>
|
|
133
139
|
</Flex>) }
|
|
134
140
|
</Td>)}
|
|
135
141
|
</Tr>)}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Status badge
|
|
4
|
+
*
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import { Typography } from '@strapi/design-system/Typography';
|
|
9
|
+
import { StatusBadge as StatusBadgeStyled } from './styles';
|
|
10
|
+
|
|
11
|
+
const StatusBadge = ({ children, textColor, ...rest }) => (<StatusBadgeStyled {...rest} >
|
|
12
|
+
<Typography variant="omega" fontWeight="semibold" textColor={textColor}>
|
|
13
|
+
{ children }
|
|
14
|
+
</Typography>
|
|
15
|
+
</StatusBadgeStyled>);
|
|
16
|
+
|
|
17
|
+
export default StatusBadge;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import styled from 'styled-components';
|
|
2
|
+
import { Badge } from '@strapi/design-system/Badge';
|
|
3
|
+
|
|
4
|
+
export const StatusBadge = styled(Badge)`
|
|
5
|
+
padding: 5px 8px;
|
|
6
|
+
border: 1px ${ ({ theme, color }) => theme.colors[`${color}200`] } solid;
|
|
7
|
+
|
|
8
|
+
overflow: hidden;
|
|
9
|
+
|
|
10
|
+
text-overflow: ellipsis;
|
|
11
|
+
|
|
12
|
+
cursor: default;
|
|
13
|
+
|
|
14
|
+
span {
|
|
15
|
+
text-transform: none;
|
|
16
|
+
}
|
|
17
|
+
`;
|