strapi-plugin-comments 2.0.0-beta.4 → 2.0.0-beta.5
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 +10 -4
- package/admin/src/components/DiscussionThread/index.js +1 -0
- package/admin/src/components/DiscussionThreadItem/index.js +3 -3
- package/admin/src/components/DiscussionThreadItemActions/index.js +22 -38
- package/admin/src/components/DiscussionThreadItemActions/styles.js +0 -4
- package/admin/src/components/DiscussionThreadItemApprovalFlowActions/index.js +72 -0
- package/admin/src/components/DiscussionThreadItemApprovalFlowActions/styles.js +6 -0
- package/admin/src/components/DiscussionThreadItemWarnings/index.js +1 -1
- package/admin/src/components/Nav/index.js +5 -1
- package/admin/src/components/icons/review.js +2 -2
- package/admin/src/pages/Details/components/DetailsEntity/index.js +66 -0
- package/admin/src/pages/Details/components/DetailsFilters/index.js +36 -0
- package/admin/src/pages/Details/components/DetailsFilters/styles.js +8 -0
- package/admin/src/pages/Details/index.js +40 -13
- package/admin/src/pages/Discover/components/DiscoverTableRow/index.js +33 -39
- package/admin/src/pages/Discover/components/DiscoverTableRow/styles.js +8 -0
- package/admin/src/pages/Discover/index.js +6 -3
- package/admin/src/pages/{Details/utils → utils}/api.js +15 -3
- package/admin/src/translations/en.json +15 -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 +3 -1
- package/admin/src/utils/renderEntryTitle.js +14 -0
- package/admin/src/utils/resolveCommentStatus.js +30 -0
- package/admin/src/utils/resolveCommentStatusColor.js +22 -0
- package/package.json +7 -3
- package/server/config/index.js +2 -1
- package/server/controllers/admin.js +3 -3
- package/server/controllers/client.js +1 -0
- package/server/services/admin.js +33 -7
- package/server/services/client.js +21 -13
- package/server/services/common.js +30 -65
- package/server/services/utils/functions.js +7 -14
- package/server/utils/constants.js +3 -3
- package/admin/src/components/EntityDetails/index.js +0 -53
package/README.md
CHANGED
|
@@ -50,9 +50,15 @@ Enjoy 🎉
|
|
|
50
50
|
|
|
51
51
|
Complete installation requirements are exact same as for Strapi itself and can be found in the documentation under [Installation Requirements](https://docs.strapi.io/developer-docs/latest/getting-started/introduction.html).
|
|
52
52
|
|
|
53
|
+
**Minimum environment requirements**
|
|
54
|
+
- Node.js `>=14.x.x`
|
|
55
|
+
- NPM `>=7.x.x`
|
|
56
|
+
|
|
57
|
+
In our minimum support we're following [official Node.js releases timelines](https://nodejs.org/en/about/releases/).
|
|
58
|
+
|
|
53
59
|
**Supported Strapi versions**:
|
|
54
60
|
|
|
55
|
-
- Strapi v4.0.
|
|
61
|
+
- Strapi v4.0.4 (recently tested)
|
|
56
62
|
|
|
57
63
|
(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.)
|
|
58
64
|
|
|
@@ -76,6 +82,7 @@ To setup amend default plugin configuration we recommend to put following snippe
|
|
|
76
82
|
comments: {
|
|
77
83
|
badWords: false,
|
|
78
84
|
moderatorRoles: ["Authenticated"],
|
|
85
|
+
approvalFlow: ['api::page.page'],
|
|
79
86
|
entryLabel: {
|
|
80
87
|
'*': ['Title', 'title', 'Name', 'name', 'Subject', 'subject'],
|
|
81
88
|
'api::page.page': ['MyField'],
|
|
@@ -87,7 +94,8 @@ To setup amend default plugin configuration we recommend to put following snippe
|
|
|
87
94
|
### Properties
|
|
88
95
|
- `badWords` - Enabled support for [bad words filtering](https://www.npmjs.com/package/bad-words). Can be turned off or overwritten using [options reference](https://www.npmjs.com/package/bad-words#constructor). Default value: `true`.
|
|
89
96
|
- `moderatorRoles` - Optional list of names of roles. Users with those roles will be notified by email when a new abuse report is created. This feature requires a built-in [Strapi email plugin](https://docs.strapi.io/developer-docs/latest/plugins/email.html) configured.
|
|
90
|
-
- `
|
|
97
|
+
- `approvalFlow` - list of Content Types which are supporting approval flow. Values must be in format like `'api::<collection name>.<content type name>'`. For not included, posted comments are going to be immediately visible.
|
|
98
|
+
- `entryLabel` - ordered list of property names per Content Type to generate related entity label. Keys must be in format like `'api::<collection name>.<content type name>'`. Default formatting set as `*`.
|
|
91
99
|
|
|
92
100
|
## Additional GQL Configuration
|
|
93
101
|
|
|
@@ -220,7 +228,6 @@ Posts a Comment related to specified instance of Content Type like for example `
|
|
|
220
228
|
},
|
|
221
229
|
"content": "My sample response",
|
|
222
230
|
"threadOf": 2, // id of comment we would like to start / continue the thread (Optional)
|
|
223
|
-
"related": "api::page.page:1"
|
|
224
231
|
}
|
|
225
232
|
```
|
|
226
233
|
|
|
@@ -230,7 +237,6 @@ Posts a Comment related to specified instance of Content Type like for example `
|
|
|
230
237
|
"authorUser": 1, // id of a author user. Optional in case of 'enableUsers: true' in the plugin configuration
|
|
231
238
|
"content": "My sample response",
|
|
232
239
|
"threadOf": 2, // id of comment we would like to start / continue the thread (Optional)
|
|
233
|
-
"related": "api::page.page:1"
|
|
234
240
|
}
|
|
235
241
|
```
|
|
236
242
|
|
|
@@ -39,6 +39,7 @@ const DiscussionThread = ({ level = [], selected = {}, isReloading }) => {
|
|
|
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}
|
|
43
44
|
root={isNil(rootThread)}
|
|
44
45
|
blockedThread={rootThread?.blockedThread || item.blockedThread}
|
|
@@ -21,7 +21,7 @@ import { getMessage, getUrl } from '../../utils';
|
|
|
21
21
|
import DiscussionThreadItemWarnings from '../DiscussionThreadItemWarnings';
|
|
22
22
|
|
|
23
23
|
const DiscussionThreadItem = (item) => {
|
|
24
|
-
const { id, isSelected, isThreadAuthor, content, root, preview, pinned, blocked, blockedThread, gotThread, threadFirstItemId, authorUser, authorAvatar, authorName } = item;
|
|
24
|
+
const { id, isSelected, isThreadAuthor, content, root, preview, pinned, blocked, removed, blockedThread, gotThread, threadFirstItemId, authorUser, authorAvatar, authorName } = item;
|
|
25
25
|
|
|
26
26
|
let background = 'neutral100';
|
|
27
27
|
let borderColor = 'neutral200';
|
|
@@ -64,14 +64,14 @@ const DiscussionThreadItem = (item) => {
|
|
|
64
64
|
</Box>
|
|
65
65
|
{ !preview && (<DiscussionThreadItemActions { ...item } root={root || pinned} />) }
|
|
66
66
|
</DiscussionThreadItemBoxContent>
|
|
67
|
-
{ !(preview || isBlocked) && (<DiscussionThreadItemWarnings { ...item } gotThread={gotThread || root} />) }
|
|
67
|
+
{ !(preview || isBlocked || removed) && (<DiscussionThreadItemWarnings { ...item } gotThread={gotThread || root} />) }
|
|
68
68
|
</Flex>
|
|
69
69
|
<Divider background={dividerColor} />
|
|
70
70
|
<DiscussionThreadItemMeta { ...item } />
|
|
71
71
|
</DiscussionThreadItemBox>
|
|
72
72
|
</DiscussionThreadItemContent>
|
|
73
73
|
|
|
74
|
-
{ (gotThread && !(preview || pinned)) && (<DiscussionThreadItemLinkBox direction="column" alignItems="flex-end">
|
|
74
|
+
{ (gotThread && !(removed || preview || pinned)) && (<DiscussionThreadItemLinkBox direction="column" alignItems="flex-end">
|
|
75
75
|
<Link to={getUrl(`discover/${threadFirstItemId}`)} endIcon={<ArrowRight />}>
|
|
76
76
|
{ getMessage('page.details.panel.discussion.nav.drilldown') }
|
|
77
77
|
</Link>
|
|
@@ -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
10
|
import { isNil } from 'lodash';
|
|
11
11
|
import { Button } from '@strapi/design-system/Button';
|
|
12
12
|
import { Flex } from '@strapi/design-system/Flex';
|
|
13
13
|
import { IconButton } from '@strapi/design-system/IconButton';
|
|
14
|
-
import { 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, approvalStatus, root }) => {
|
|
24
24
|
|
|
25
25
|
const [blockConfirmationVisible, setBlockConfirmationVisible] = useState(false);
|
|
26
26
|
const [blockThreadConfirmationVisible, setBlockThreadConfirmationVisible] = useState(false);
|
|
@@ -71,35 +71,16 @@ 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');
|
|
74
75
|
|
|
75
76
|
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
|
-
|
|
77
|
+
const status = resolveCommentStatus({ removed, blocked, blockedThread, approvalStatus });
|
|
97
78
|
return {
|
|
98
|
-
badgeVisible: isBlocked || gotApprovalFlow,
|
|
99
|
-
badgeColor:
|
|
100
|
-
badgeLabel:
|
|
101
|
-
}
|
|
102
|
-
}
|
|
79
|
+
badgeVisible: isBlocked || gotApprovalFlow || removed,
|
|
80
|
+
badgeColor: resolveCommentStatusColor(status),
|
|
81
|
+
badgeLabel: status,
|
|
82
|
+
};
|
|
83
|
+
};
|
|
103
84
|
|
|
104
85
|
const {badgeVisible, badgeColor, badgeLabel } = resolveStatusBadge();
|
|
105
86
|
|
|
@@ -129,17 +110,20 @@ const DiscussionThreadItemActions = ({ id, blocked, blockedThread, gotThread, pi
|
|
|
129
110
|
unblockItemThreadMutation.mutate(id);
|
|
130
111
|
};
|
|
131
112
|
|
|
113
|
+
if (removed || isRejected) {
|
|
114
|
+
return (<DiscussionThreadItemActionsWrapper as={Flex} direction="row">
|
|
115
|
+
<DiscussionThreadItemActionsBadge color={badgeColor} backgroundColor={`${badgeColor}100`} textColor={`${badgeColor}600`}>{getMessage(`page.common.item.status.${badgeLabel}`, badgeLabel)}</DiscussionThreadItemActionsBadge>
|
|
116
|
+
</DiscussionThreadItemActionsWrapper>);
|
|
117
|
+
}
|
|
118
|
+
|
|
132
119
|
return (<>
|
|
133
120
|
<DiscussionThreadItemActionsWrapper as={Flex} direction="row">
|
|
134
|
-
{ badgeVisible && (<DiscussionThreadItemActionsBadge color={badgeColor} backgroundColor={`${badgeColor}100`} textColor={`${badgeColor}600`}>{getMessage(`page.
|
|
121
|
+
{ badgeVisible && (<DiscussionThreadItemActionsBadge color={badgeColor} backgroundColor={`${badgeColor}100`} textColor={`${badgeColor}600`}>{getMessage(`page.common.item.status.${badgeLabel}`, badgeLabel)}</DiscussionThreadItemActionsBadge>)}
|
|
135
122
|
{ !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')} />)}
|
|
123
|
+
{ !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
124
|
{ (!blockedThread && (gotThread || pinned)) && (<Button onClick={handleBlockThreadClick} startIcon={<LockIcon />} loading={blockItemThreadMutation.isLoading} variant="danger">{ getMessage('page.details.actions.thread.block', 'Block thread') }</Button>) }
|
|
138
125
|
{ (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>) }
|
|
126
|
+
{ needsApproval && (<DiscussionThreadItemApprovalFlowActions id={id} queryToInvalidate="get-details-data" />) }
|
|
143
127
|
</DiscussionThreadItemActionsWrapper>
|
|
144
128
|
{ !blocked && (<ConfirmationDialog
|
|
145
129
|
isVisible={blockConfirmationVisible}
|
|
@@ -0,0 +1,72 @@
|
|
|
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, queryToInvalidate }) => {
|
|
19
|
+
|
|
20
|
+
const toggleNotification = useNotification();
|
|
21
|
+
const queryClient = useQueryClient();
|
|
22
|
+
const { lockApp, unlockApp } = useOverlayBlocker();
|
|
23
|
+
|
|
24
|
+
const onSuccess = (message, stateAction = () => {}) => 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
|
+
|
|
49
|
+
const handleApproveClick = () => {
|
|
50
|
+
lockApp();
|
|
51
|
+
approveItemMutation.mutate(id);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const handleRejectClick = () => {
|
|
55
|
+
lockApp();
|
|
56
|
+
rejectItemMutation.mutate(id);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return (<DiscussionThreadItemActionsGroupWrapper>
|
|
60
|
+
<IconButton icon={<Check />} label={getMessage('page.details.actions.comment.approve', 'Approve')} onClick={handleApproveClick} />
|
|
61
|
+
<IconButton icon={<Cross />} label={getMessage('page.details.actions.comment.reject', 'Reject')} onClick={handleRejectClick} />
|
|
62
|
+
</DiscussionThreadItemActionsGroupWrapper>);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
DiscussionThreadItemApprovalFlowActions.propTypes = {
|
|
66
|
+
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
|
67
|
+
queryToInvalidate: PropTypes.string,
|
|
68
|
+
wrapped: PropTypes.bool,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export default DiscussionThreadItemApprovalFlowActions;
|
|
72
|
+
|
|
@@ -15,7 +15,7 @@ import { Flex } from '@strapi/design-system/Flex';
|
|
|
15
15
|
import { Typography } from '@strapi/design-system/Typography';
|
|
16
16
|
import { useNotification, useOverlayBlocker } from '@strapi/helper-plugin';
|
|
17
17
|
import { getMessage, handleAPIError } from '../../utils';
|
|
18
|
-
import { blockItem, blockItemThread, resolveReport } from '../../pages/
|
|
18
|
+
import { blockItem, blockItemThread, resolveReport } from '../../pages/utils/api';
|
|
19
19
|
import { DiscussionThreadItemWarningsWrapper } from './styles';
|
|
20
20
|
import ReportsReviewModal from '../ReportsReviewModal';
|
|
21
21
|
import ReportsReviewTable from '../ReportsReviewTable';
|
|
@@ -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>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
import React from 'react';
|
|
3
3
|
|
|
4
|
-
const initSize =
|
|
4
|
+
const initSize = 16;
|
|
5
5
|
|
|
6
6
|
const ReviewIcon = ({ width, height, size = 16 }) =>
|
|
7
7
|
<svg
|
|
@@ -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 fill-rule="evenodd" clip-rule="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
|
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Entity Details
|
|
4
|
+
*
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import PropTypes from 'prop-types';
|
|
9
|
+
import { capitalize, isNil, first, take } from 'lodash';
|
|
10
|
+
import { Box } from '@strapi/design-system/Box';
|
|
11
|
+
import { Divider } from '@strapi/design-system/Divider';
|
|
12
|
+
import { Flex } from '@strapi/design-system/Flex';
|
|
13
|
+
import { Stack } from '@strapi/design-system/Stack';
|
|
14
|
+
import { Typography } from '@strapi/design-system/Typography';
|
|
15
|
+
import { getMessage } from '../../../../utils';
|
|
16
|
+
|
|
17
|
+
const DetailsEntity = ({ data = {}, schema = {}, config = {} }) => {
|
|
18
|
+
const { entryLabel = {} } = config;
|
|
19
|
+
const { attributes = {} } = schema;
|
|
20
|
+
const keys = Object.keys(attributes);
|
|
21
|
+
const entityLabelKey = first(entryLabel[data?.uid]);
|
|
22
|
+
|
|
23
|
+
if (keys.length === 0) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const FIELDS_LIMIT = 5;
|
|
28
|
+
const itemKeys = take(
|
|
29
|
+
keys.filter(_ => (attributes[_].type === 'string') && !isNil(data[_]) && (_ !== entityLabelKey)),
|
|
30
|
+
FIELDS_LIMIT
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const formatLabel = label => label.split('_')
|
|
34
|
+
.map(_ => capitalize(_))
|
|
35
|
+
.join(' ');
|
|
36
|
+
|
|
37
|
+
return (<Box padding={4}>
|
|
38
|
+
<Typography variant="sigma" textColor="neutral600" id="entity-details">
|
|
39
|
+
{ getMessage('page.details.panel.entity', 'Details') }
|
|
40
|
+
</Typography>
|
|
41
|
+
<Box paddingTop={2} paddingBottom={6}>
|
|
42
|
+
<Divider />
|
|
43
|
+
</Box>
|
|
44
|
+
<Stack size={itemKeys.length}>
|
|
45
|
+
<Flex direction="column" alignItems="flex-start">
|
|
46
|
+
<Typography fontWeight="bold">{ formatLabel(entityLabelKey) }</Typography>
|
|
47
|
+
<Typography>{ data[entityLabelKey] }</Typography>
|
|
48
|
+
</Flex>
|
|
49
|
+
{ itemKeys.map(_ =>
|
|
50
|
+
(<Flex direction="column" alignItems="flex-start">
|
|
51
|
+
<Typography fontWeight="bold">{ formatLabel(_) }</Typography>
|
|
52
|
+
<Typography>{ data[_] }</Typography>
|
|
53
|
+
</Flex>))
|
|
54
|
+
}
|
|
55
|
+
</Stack>
|
|
56
|
+
</Box>);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
DetailsEntity.propTypes = {
|
|
60
|
+
data: PropTypes.object.isRequired,
|
|
61
|
+
schema: PropTypes.object.isRequired,
|
|
62
|
+
config: PropTypes.object.isRequired,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export default DetailsEntity;
|
|
66
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { Box } from '@strapi/design-system/Box';
|
|
4
|
+
import { Flex } from '@strapi/design-system/Flex';
|
|
5
|
+
import { Stack } from '@strapi/design-system/Stack';
|
|
6
|
+
import { Switch } from '@strapi/design-system/Switch';
|
|
7
|
+
import { Typography } from '@strapi/design-system/Typography';
|
|
8
|
+
import { getMessage } from '../../../../utils';
|
|
9
|
+
|
|
10
|
+
const DetailsFilters = ({ data, onChange }) => {
|
|
11
|
+
|
|
12
|
+
const { removed = false } = data;
|
|
13
|
+
|
|
14
|
+
const handleOnChange = (prop, value) => onChange({
|
|
15
|
+
...data,
|
|
16
|
+
[prop]: !value ? undefined : value,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
return (<Box as={Flex}>
|
|
20
|
+
<Stack size={2} horizontal>
|
|
21
|
+
<Typography variant="epsilon">{getMessage('page.details.filters.label', '')}</Typography>
|
|
22
|
+
<Switch
|
|
23
|
+
onLabel={getMessage('page.details.filters.removed.show', 'Show')}
|
|
24
|
+
offLabel={getMessage('page.details.filters.removed.hide', 'Hide')}
|
|
25
|
+
selected={removed} onChange={() => handleOnChange('removed', !removed)}
|
|
26
|
+
visibleLabels />
|
|
27
|
+
</Stack>
|
|
28
|
+
</Box>);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
DetailsFilters.propTypes = {
|
|
32
|
+
data: PropTypes.object.isRequired,
|
|
33
|
+
onChange: PropTypes.func.isRequired,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default DetailsFilters;
|
|
@@ -4,11 +4,15 @@
|
|
|
4
4
|
*
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import React, { useRef, useMemo } from 'react';
|
|
7
|
+
import React, { memo, useRef, useMemo, useState } from 'react';
|
|
8
|
+
import { connect } from 'react-redux';
|
|
9
|
+
import { bindActionCreators, compose } from 'redux';
|
|
10
|
+
import isEqual from 'react-fast-compare';
|
|
8
11
|
import { useQuery } from 'react-query';
|
|
9
12
|
import { useRouteMatch } from 'react-router-dom';
|
|
10
13
|
import { Layout, HeaderLayout, TwoColsLayout, ContentLayout } from '@strapi/design-system/Layout';
|
|
11
14
|
import { Box } from '@strapi/design-system/Box';
|
|
15
|
+
import { Flex } from '@strapi/design-system/Flex';
|
|
12
16
|
import { Link } from '@strapi/design-system/Link';
|
|
13
17
|
import { Loader } from '@strapi/design-system/Loader';
|
|
14
18
|
import { Stack } from '@strapi/design-system/Stack';
|
|
@@ -24,16 +28,17 @@ import {
|
|
|
24
28
|
useFocusWhenNavigate,
|
|
25
29
|
} from '@strapi/helper-plugin';
|
|
26
30
|
import getMessage from '../../utils/getMessage';
|
|
27
|
-
import { fetchDetailsData, fetchContentTypeData } from '
|
|
31
|
+
import { fetchDetailsData, fetchContentTypeData } from '../utils/api';
|
|
28
32
|
import pluginPermissions from '../../permissions';
|
|
29
33
|
import { getUrl } from '../../utils';
|
|
30
34
|
import Nav from '../../components/Nav';
|
|
31
|
-
import
|
|
32
|
-
import EntityDetails from '../../components/EntityDetails';
|
|
35
|
+
import DetailsEntity from './components/DetailsEntity';
|
|
33
36
|
import DiscussionThread from '../../components/DiscussionThread';
|
|
34
37
|
import DiscussionStatus from '../../components/DiscussionStatus';
|
|
38
|
+
import makeAppView from '../App/reducer/selectors';
|
|
39
|
+
import DetailsFilters from './components/DetailsFilters';
|
|
35
40
|
|
|
36
|
-
const Details = () => {
|
|
41
|
+
const Details = ({ config }) => {
|
|
37
42
|
useFocusWhenNavigate();
|
|
38
43
|
|
|
39
44
|
const {
|
|
@@ -54,16 +59,20 @@ const Details = () => {
|
|
|
54
59
|
allowedActions: { canView },
|
|
55
60
|
} = useRBAC(viewPermissions);
|
|
56
61
|
|
|
62
|
+
const [filters, setFilters] = useState({});
|
|
63
|
+
|
|
64
|
+
const regexUID = new RegExp(config.regex.uid);
|
|
65
|
+
|
|
57
66
|
const { isLoading: isLoadingForData, data, isFetching } = useQuery(
|
|
58
|
-
['get-details-data', id],
|
|
59
|
-
() => fetchDetailsData(id, toggleNotification),
|
|
67
|
+
['get-details-data', id, filters],
|
|
68
|
+
() => fetchDetailsData(id, filters, toggleNotification),
|
|
60
69
|
{
|
|
61
70
|
initialData: { },
|
|
62
71
|
}
|
|
63
72
|
);
|
|
64
73
|
|
|
65
74
|
const { entity, level, selected } = data;
|
|
66
|
-
const entityUidValid = entity?.uid &&
|
|
75
|
+
const entityUidValid = entity?.uid && regexUID.test(entity.uid);
|
|
67
76
|
|
|
68
77
|
const { data: contentTypeData } = useQuery(
|
|
69
78
|
['get-additional-data', entity?.uid],
|
|
@@ -74,10 +83,12 @@ const Details = () => {
|
|
|
74
83
|
}
|
|
75
84
|
)
|
|
76
85
|
|
|
86
|
+
const handleChangeFilters = props => setFilters(props);
|
|
87
|
+
|
|
77
88
|
const isLoading = isLoadingForData || isFetching;
|
|
78
89
|
|
|
79
90
|
return <Box background="neutral100">
|
|
80
|
-
<Layout
|
|
91
|
+
<Layout>
|
|
81
92
|
{(isLoading || isLoadingForPermissions) && isEmpty(data) ? (<LoadingIndicatorPage />) : (
|
|
82
93
|
<>
|
|
83
94
|
<HeaderLayout
|
|
@@ -86,16 +97,19 @@ const Details = () => {
|
|
|
86
97
|
{ getMessage('HeaderLayout.link.go-back', 'Back', false) }
|
|
87
98
|
</Link>}
|
|
88
99
|
title={ getMessage('page.details.header') }
|
|
89
|
-
subtitle={ getMessage('page.details.header.description') }
|
|
100
|
+
subtitle={ getMessage('page.details.header.description') }
|
|
101
|
+
primaryAction={<DetailsFilters data={filters} onChange={handleChangeFilters} />}
|
|
102
|
+
secondaryAction={<Box as={Flex}> </Box>}
|
|
103
|
+
as="h2" />
|
|
90
104
|
<ContentLayout>
|
|
91
105
|
<TwoColsLayout
|
|
92
106
|
startCol={<DiscussionThread level={level} selected={selected} isReloading={isLoading} />}
|
|
93
107
|
endCol={ <Stack size={2}>
|
|
94
108
|
<DiscussionStatus item={selected} />
|
|
95
109
|
{ !contentTypeData?.schema ? (<Box padding={4}>
|
|
96
|
-
<Loader small>
|
|
110
|
+
<Loader small>{ getMessage('page.details.panel.loading', 'Loading...') }</Loader>
|
|
97
111
|
</Box>) :
|
|
98
|
-
(<
|
|
112
|
+
(<DetailsEntity data={entity} schema={contentTypeData?.schema} config={config} />)
|
|
99
113
|
}
|
|
100
114
|
</Stack>
|
|
101
115
|
}
|
|
@@ -106,4 +120,17 @@ const Details = () => {
|
|
|
106
120
|
</Box>;
|
|
107
121
|
}
|
|
108
122
|
|
|
109
|
-
|
|
123
|
+
const mapStateToProps = makeAppView();
|
|
124
|
+
|
|
125
|
+
export function mapDispatchToProps(dispatch) {
|
|
126
|
+
return bindActionCreators(
|
|
127
|
+
{ },
|
|
128
|
+
dispatch
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
const withConnect = connect(
|
|
132
|
+
mapStateToProps,
|
|
133
|
+
mapDispatchToProps
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
export default compose(withConnect)(memo(Details, isEqual));
|