strapi-plugin-comments 2.0.0-beta.3 → 2.0.0-beta.7

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.
Files changed (73) hide show
  1. package/README.md +335 -11
  2. package/admin/src/components/DiscussionThread/index.js +9 -2
  3. package/admin/src/components/DiscussionThreadItem/index.js +27 -12
  4. package/admin/src/components/DiscussionThreadItemActions/index.js +61 -50
  5. package/admin/src/components/DiscussionThreadItemActions/styles.js +0 -4
  6. package/admin/src/components/DiscussionThreadItemApprovalFlowActions/index.js +81 -0
  7. package/admin/src/components/DiscussionThreadItemApprovalFlowActions/styles.js +6 -0
  8. package/admin/src/components/DiscussionThreadItemMeta/index.js +16 -5
  9. package/admin/src/components/DiscussionThreadItemWarnings/index.js +16 -10
  10. package/admin/src/components/Nav/index.js +5 -1
  11. package/admin/src/components/ReportsReviewModal/index.js +5 -0
  12. package/admin/src/components/ReportsReviewTable/index.js +60 -52
  13. package/admin/src/components/icons/review.js +2 -2
  14. package/admin/src/index.js +1 -1
  15. package/admin/src/pages/Details/components/DetailsEntity/index.js +66 -0
  16. package/admin/src/pages/Details/components/DetailsFilters/index.js +37 -0
  17. package/admin/src/pages/Details/components/DetailsFilters/styles.js +8 -0
  18. package/admin/src/pages/Details/index.js +75 -42
  19. package/admin/src/pages/Discover/components/DiscoverTableRow/index.js +69 -48
  20. package/admin/src/pages/Discover/components/DiscoverTableRow/styles.js +8 -0
  21. package/admin/src/pages/Discover/index.js +72 -56
  22. package/admin/src/pages/{Details/utils → utils}/api.js +15 -3
  23. package/admin/src/permissions.js +10 -8
  24. package/admin/src/translations/en.json +17 -4
  25. package/admin/src/utils/constants.js +3 -6
  26. package/admin/src/utils/getMessage.js +1 -1
  27. package/admin/src/utils/getRelatedGroups.js +1 -3
  28. package/admin/src/utils/index.js +6 -2
  29. package/admin/src/utils/renderEntryTitle.js +14 -0
  30. package/admin/src/utils/renderInitials.js +5 -0
  31. package/admin/src/utils/resolveCommentStatus.js +30 -0
  32. package/admin/src/utils/resolveCommentStatusColor.js +22 -0
  33. package/package.json +8 -3
  34. package/permissions.js +19 -0
  35. package/server/bootstrap.js +38 -2
  36. package/server/config/index.js +9 -3
  37. package/server/controllers/admin.js +3 -3
  38. package/server/controllers/client.js +4 -18
  39. package/server/controllers/utils/parsers.js +17 -0
  40. package/server/graphql/index.js +26 -0
  41. package/server/graphql/mutations/createAbuseReport.js +20 -0
  42. package/server/graphql/mutations/createComment.js +20 -0
  43. package/server/graphql/mutations/index.js +21 -0
  44. package/server/graphql/mutations/removeComment.js +20 -0
  45. package/server/graphql/mutations/updateComment.js +20 -0
  46. package/server/graphql/queries/findAllFlat.js +29 -0
  47. package/server/graphql/queries/findAllInHierarchy.js +31 -0
  48. package/server/graphql/queries/index.js +19 -0
  49. package/server/graphql/resolvers-config.js +10 -0
  50. package/server/graphql/types/comment-author-user.js +9 -0
  51. package/server/graphql/types/comment-author.js +9 -0
  52. package/server/graphql/types/comment-nested.js +16 -0
  53. package/server/graphql/types/comment.js +16 -0
  54. package/server/graphql/types/create-comment-author.js +9 -0
  55. package/server/graphql/types/create-comment.js +9 -0
  56. package/server/graphql/types/create-report.js +9 -0
  57. package/server/graphql/types/id.js +24 -0
  58. package/server/graphql/types/identify-comment-author.js +6 -0
  59. package/server/graphql/types/index.js +17 -0
  60. package/server/graphql/types/remove-comment.js +8 -0
  61. package/server/graphql/types/report-reason.js +10 -0
  62. package/server/graphql/types/report.js +11 -0
  63. package/server/graphql/types/update-comment.js +9 -0
  64. package/server/register.js +3 -1
  65. package/server/services/admin.js +42 -15
  66. package/server/services/client.js +54 -38
  67. package/server/services/common.js +38 -67
  68. package/server/services/graphql.js +190 -0
  69. package/server/services/index.js +2 -0
  70. package/server/services/utils/functions.js +24 -17
  71. package/server/utils/constants.js +3 -3
  72. package/strapi-server.js +33 -0
  73. package/admin/src/components/EntityDetails/index.js +0 -53
@@ -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 key={`prop_${_}`} 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,37 @@
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
+ label={getMessage('page.details.filters.removed.label', 'Comments visibility')}
24
+ onLabel={getMessage('page.details.filters.removed.show', 'Show')}
25
+ offLabel={getMessage('page.details.filters.removed.hide', 'Hide')}
26
+ selected={removed} onChange={() => handleOnChange('removed', !removed)}
27
+ visibleLabels />
28
+ </Stack>
29
+ </Box>);
30
+ };
31
+
32
+ DetailsFilters.propTypes = {
33
+ data: PropTypes.object.isRequired,
34
+ onChange: PropTypes.func.isRequired,
35
+ };
36
+
37
+ export default DetailsFilters;
@@ -0,0 +1,8 @@
1
+ import styled from 'styled-components';
2
+ import { Link } from '@strapi/design-system/Link';
3
+
4
+ export const TableLink = styled(Link)`
5
+ display: block;
6
+ text-overflow: ellipsis;
7
+ overflow: hidden;
8
+ `
@@ -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 './utils/api';
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 { REGEX } from '../../utils/constants';
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 {
@@ -45,28 +50,35 @@ const Details = () => {
45
50
  const trackUsageRef = useRef(trackUsage);
46
51
  const toggleNotification = useNotification();
47
52
 
48
- const viewPermissions = useMemo(() => {
49
- return { view: pluginPermissions.view };
50
- }, []);
53
+ const viewPermissions = useMemo(() => ({
54
+ access: pluginPermissions.access,
55
+ moderate: pluginPermissions.moderate,
56
+ accessReports: pluginPermissions.reports,
57
+ reviewReports: pluginPermissions.reportsReview,
58
+ }), []);
51
59
 
52
60
  const {
53
61
  isLoading: isLoadingForPermissions,
54
- allowedActions: { canView },
62
+ allowedActions: { canAccess, canModerate, canAccessReports, canReviewReports },
55
63
  } = useRBAC(viewPermissions);
56
64
 
65
+ const [filters, setFilters] = useState({});
66
+
67
+ const regexUID = new RegExp(config.regex.uid);
68
+
57
69
  const { isLoading: isLoadingForData, data, isFetching } = useQuery(
58
- ['get-details-data', id],
59
- () => fetchDetailsData(id, toggleNotification),
70
+ ['get-details-data', id, filters, canAccess],
71
+ () => fetchDetailsData(id, filters, toggleNotification),
60
72
  {
61
73
  initialData: { },
62
74
  }
63
75
  );
64
76
 
65
77
  const { entity, level, selected } = data;
66
- const entityUidValid = entity?.uid && REGEX.uid.test(entity?.uid);
78
+ const entityUidValid = entity?.uid && regexUID.test(entity.uid);
67
79
 
68
80
  const { data: contentTypeData } = useQuery(
69
- ['get-additional-data', entity?.uid],
81
+ ['get-additional-data', entity?.uid, canAccess],
70
82
  () => fetchContentTypeData(entity?.uid, toggleNotification),
71
83
  {
72
84
  enabled: !!entity?.uid,
@@ -74,36 +86,57 @@ const Details = () => {
74
86
  }
75
87
  )
76
88
 
89
+ const handleChangeFilters = props => setFilters(props);
90
+
77
91
  const isLoading = isLoadingForData || isFetching;
78
92
 
79
- return <Box background="neutral100">
80
- <Layout sideNav={<Nav />}>
81
- {(isLoading || isLoadingForPermissions) && isEmpty(data) ? (<LoadingIndicatorPage />) : (
82
- <>
83
- <HeaderLayout
84
- navigationAction={
85
- <Link startIcon={<ArrowLeft />} to={getUrl(`discover`)}>
86
- { getMessage('HeaderLayout.link.go-back', 'Back', false) }
87
- </Link>}
88
- title={ getMessage('page.details.header') }
89
- subtitle={ getMessage('page.details.header.description') } as="h2" />
90
- <ContentLayout>
91
- <TwoColsLayout
92
- startCol={<DiscussionThread level={level} selected={selected} isReloading={isLoading} />}
93
- endCol={ <Stack size={2}>
94
- <DiscussionStatus item={selected} />
95
- { !contentTypeData?.schema ? (<Box padding={4}>
96
- <Loader small>Fetching entity...</Loader>
97
- </Box>) :
98
- (<EntityDetails data={entity} schema={contentTypeData?.schema} />)
99
- }
100
- </Stack>
101
- }
102
- />
103
- </ContentLayout>
104
- </>) }
105
- </Layout>
106
- </Box>;
93
+ if (canAccess) {
94
+ return <Box background="neutral100">
95
+ <Layout>
96
+ {(isLoading || isLoadingForPermissions) && isEmpty(data) ? (<LoadingIndicatorPage />) : (
97
+ <>
98
+ <HeaderLayout
99
+ navigationAction={
100
+ <Link startIcon={<ArrowLeft />} to={getUrl(`discover`)}>
101
+ { getMessage('HeaderLayout.link.go-back', 'Back', false) }
102
+ </Link>}
103
+ title={ getMessage('page.details.header') }
104
+ subtitle={ getMessage('page.details.header.description') }
105
+ primaryAction={<DetailsFilters data={filters} onChange={handleChangeFilters} />}
106
+ secondaryAction={<Box as={Flex}>&nbsp;</Box>}
107
+ as="h2" />
108
+ <ContentLayout>
109
+ <TwoColsLayout
110
+ startCol={<DiscussionThread level={level} selected={selected} isReloading={isLoading} allowedActions={{ canModerate, canAccessReports, canReviewReports }} />}
111
+ endCol={ <Stack size={2}>
112
+ <DiscussionStatus item={selected} />
113
+ { !contentTypeData?.schema ? (<Box padding={4}>
114
+ <Loader small>{ getMessage('page.details.panel.loading', 'Loading...') }</Loader>
115
+ </Box>) :
116
+ (<DetailsEntity data={entity} schema={contentTypeData?.schema} config={config} />)
117
+ }
118
+ </Stack>
119
+ }
120
+ />
121
+ </ContentLayout>
122
+ </>) }
123
+ </Layout>
124
+ </Box>;
125
+ }
126
+ return null;
127
+ }
128
+
129
+ const mapStateToProps = makeAppView();
130
+
131
+ export function mapDispatchToProps(dispatch) {
132
+ return bindActionCreators(
133
+ { },
134
+ dispatch
135
+ );
107
136
  }
137
+ const withConnect = connect(
138
+ mapStateToProps,
139
+ mapDispatchToProps
140
+ );
108
141
 
109
- export default Details;
142
+ export default compose(withConnect)(memo(Details, isEqual));
@@ -2,7 +2,8 @@ import React, { useState } from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import { useIntl } from 'react-intl';
4
4
  import { useMutation, useQueryClient } from 'react-query';
5
- import { first, isEmpty, orderBy } from 'lodash';
5
+ import { isNil, isEmpty, orderBy } from 'lodash';
6
+ import { Avatar, Initials } from '@strapi/design-system/Avatar';
6
7
  import { Badge } from '@strapi/design-system/Badge';
7
8
  import { Box } from '@strapi/design-system/Box';
8
9
  import { Button } from '@strapi/design-system/Button';
@@ -13,17 +14,19 @@ import { Link } from '@strapi/design-system/Link';
13
14
  import { Stack } from '@strapi/design-system/Stack';
14
15
  import { Tr, Td } from '@strapi/design-system/Table';
15
16
  import { Typography } from '@strapi/design-system/Typography';
16
- import { Eye, Lock } from '@strapi/icons';
17
+ import { Eye } from '@strapi/icons';
17
18
  import { useNotification, useOverlayBlocker } from '@strapi/helper-plugin';
18
- import { getMessage, handleAPIError } from '../../../../utils';
19
- import { COMMENT_STATUS } from '../../../../utils/constants';
19
+ import { getMessage, getUrl, handleAPIError, renderInitials, resolveCommentStatus, resolveCommentStatusColor } from '../../../../utils';
20
20
  import ReportsReviewModal from '../../../../components/ReportsReviewModal';
21
21
  import ReportsReviewTable from '../../../../components/ReportsReviewTable';
22
- import { blockItem, blockItemThread, resolveReport } from '../../../Details/utils/api';
22
+ import { blockItem, blockItemThread, resolveReport } from '../../../utils/api';
23
23
  import pluginId from '../../../../pluginId';
24
24
  import { ReviewIcon, LockIcon } from '../../../../components/icons';
25
+ import { TableLink } from './styles';
26
+ import renderEntryTitle from '../../../../utils/renderEntryTitle';
27
+ import DiscussionThreadItemApprovalFlowActions from '../../../../components/DiscussionThreadItemApprovalFlowActions';
25
28
 
26
- const DiscoverTableRow = ({ config, item, onClick }) => {
29
+ const DiscoverTableRow = ({ config, item, allowedActions: { canModerate, canAccessReports, canReviewReports }, onClick }) => {
27
30
 
28
31
  const { id, reports } = item;
29
32
 
@@ -72,14 +75,22 @@ const DiscoverTableRow = ({ config, item, onClick }) => {
72
75
  refetchActive: false,
73
76
  });
74
77
 
75
- const handleReportsReviewClick = () => setReportsReviewVisible(true);
78
+ const handleReportsReviewClick = () => {
79
+ if (canAccessReports) {
80
+ setReportsReviewVisible(true);
81
+ }
82
+ };
76
83
  const handleBlockItemClick = async () => {
77
- lockApp();
78
- blockItemMutation.mutate(id);
84
+ if (canModerate) {
85
+ lockApp();
86
+ blockItemMutation.mutate(id);
87
+ }
79
88
  };
80
89
  const handleBlockItemThreadClick = async () => {
81
- lockApp();
82
- blockItemThreadMutation.mutate(id);
90
+ if (canModerate) {
91
+ lockApp();
92
+ blockItemThreadMutation.mutate(id);
93
+ }
83
94
  };
84
95
  const handleReportsReviewClose = async () => {
85
96
  await queryClient.invalidateQueries('get-data');
@@ -89,59 +100,56 @@ const DiscoverTableRow = ({ config, item, onClick }) => {
89
100
  const isLoading = blockItemMutation.isLoading || blockItemThreadMutation.isLoading || resolveReportMutation.isLoading;
90
101
  const openReports = reports?.filter(_ => !_.resolved);
91
102
  const hasReports = !isEmpty(openReports);
92
- const reviewFlowEnabled = hasReports && !(item.blocked || item.blockedThread);
103
+ const reviewFlowEnabled = (canAccessReports && hasReports) && !(item.blocked || item.blockedThread);
93
104
 
94
105
  const handleClick = () => onClick();
95
106
  const handleBlockButtonsStateChange = disabled => setBlockButtonsDisabled(disabled);
96
107
 
97
- const renderStatus = ({ blocked, blockedThread }) => {
98
- let status = blocked || blockedThread ? COMMENT_STATUS.BLOCKED : COMMENT_STATUS.OPEN;
99
- if (reviewFlowEnabled) {
100
- status = COMMENT_STATUS.TO_REVIEW;
101
- }
102
- let color = 'secondary';
103
- switch (status) {
104
- case COMMENT_STATUS.OPEN:
105
- color = 'success';
106
- break;
107
- case COMMENT_STATUS.TO_REVIEW:
108
- color = 'warning';
109
- break;
110
- case COMMENT_STATUS.BLOCKED:
111
- color = 'danger';
112
- break;
113
- };
108
+ const renderStatus = (props) => {
109
+ const status = resolveCommentStatus({ ...props, reviewFlowEnabled });
110
+ const color = resolveCommentStatusColor(status);
111
+
114
112
  return (<Badge backgroundColor={`${color}100`} textColor={`${color}600`}>{ getMessage({
115
- id: `page.discover.table.header.status.${status}`,
113
+ id: `page.common.item.status.${status}`,
116
114
  props: {
117
115
  count: openReports.length
118
116
  }
119
117
  }, status) }</Badge>);
120
118
  };
121
119
 
122
- const renderEntryTitle = entry => {
123
- const { entryLabel } = config;
124
- const rule = entry.uid in entryLabel ? entryLabel[entry.uid] : entryLabel['*'];
125
- return first(
126
- Object.keys(entry)
127
- .filter(_ => (rule === _) || rule.includes(_))
128
- .map(_ => entry[_])
129
- .filter(_ => _)
130
- );
131
- };
132
-
133
120
  const renderEntryUrl = entry => `/content-manager/collectionType/${entry.uid}/${entry.id}`;
121
+ const renderDetailsUrl = entry => getUrl(`discover/${entry.id}`);
122
+
123
+ const gotApprovalFlow = !isNil(item.approvalStatus);
124
+ const needsApproval = gotApprovalFlow && (item.approvalStatus === 'PENDING');
125
+
126
+ let actionItemsCount = 1;
127
+ if(reviewFlowEnabled || needsApproval) {
128
+ actionItemsCount = 2;
129
+ }
134
130
 
135
131
  return (
136
132
  <Tr key={item.id}>
137
- <Td style={{ maxWidth: '40vw' }}>
138
- <Typography textColor="neutral800" ellipsis>{item.content}</Typography>
133
+ <Td>
134
+ <Typography textColor="neutral800" fontWeight="bold">#{ item.id }</Typography>
139
135
  </Td>
140
136
  <Td>
141
- <Typography textColor="neutral800">{item.threadOf?.id || '-'}</Typography>
137
+ <Stack size={2} horizontal>
138
+ { item.author?.avatar ? <Avatar src={item.author?.avatar} alt={item.author.name} /> : <Initials>{ renderInitials(item.author.name) }</Initials> }
139
+ <Typography textColor="neutral800" variant="pi">{ item.author.name }</Typography>
140
+ </Stack>
141
+ </Td>
142
+ <Td style={{ maxWidth: '30vw' }}>
143
+ <Typography textColor="neutral800" ellipsis>{item.content}</Typography>
142
144
  </Td>
143
145
  <Td>
144
- <Link to={ renderEntryUrl(item.related) }>{ renderEntryTitle(item.related) }</Link>
146
+ { item.threadOf?.id ? (<Link to={ renderDetailsUrl(item.threadOf) }>{ getMessage({
147
+ id: 'page.discover.table.cell.thread',
148
+ props: { id: item.threadOf.id },
149
+ }, '#' + item.threadOf.id) }</Link>) : '-' }
150
+ </Td>
151
+ <Td style={{ maxWidth: '15vw' }}>
152
+ <TableLink to={ renderEntryUrl(item.related) }>{ renderEntryTitle(item.related, config) }</TableLink>
145
153
  </Td>
146
154
  <Td>
147
155
  <Typography textColor="neutral800">{ formatDate(item.updatedAt || item.createdAt, { dateStyle: 'long', timeStyle: 'short' }) }</Typography>
@@ -151,23 +159,30 @@ const DiscoverTableRow = ({ config, item, onClick }) => {
151
159
  </Td>
152
160
  <Td>
153
161
  <Flex direction="column" alignItems="flex-end">
154
- <Stack size={reviewFlowEnabled ? 2 : 1} horizontal>
162
+ <Stack size={actionItemsCount} horizontal>
155
163
  { reviewFlowEnabled && (<IconButton onClick={handleReportsReviewClick} label={ getMessage('page.discover.table.reports.review') } icon={<ReviewIcon />} />)}
164
+ { (canModerate && needsApproval) && (
165
+ <DiscussionThreadItemApprovalFlowActions
166
+ id={item.id}
167
+ allowedActions={{ canModerate }}
168
+ queryToInvalidate="get-data" />
169
+ )}
156
170
  <IconButton onClick={handleClick} label={ getMessage('page.discover.table.action.display') } icon={<Eye />} />
157
171
  </Stack>
158
172
  </Flex>
159
173
  { reviewFlowEnabled && (<ReportsReviewModal
160
174
  isVisible={reportsReviewVisible}
161
175
  isActionAsync={isLoading}
176
+ allowedActions={{ canModerate, canAccessReports, canReviewReports }}
162
177
  onClose={handleReportsReviewClose}
163
- startActions={<>
178
+ startActions={canModerate && (<>
164
179
  <Button onClick={handleBlockItemClick} variant="danger-light" startIcon={<LockIcon />} disabled={blockButtonsDisabled}>
165
180
  { getMessage(`page.details.actions.comment.block`, 'Block comment') }
166
181
  </Button>
167
182
  { item.gotThread && (<Button onClick={handleBlockItemThreadClick} variant="danger" startIcon={<LockIcon />} disabled={blockButtonsDisabled}>
168
183
  { getMessage(`page.details.actions.thread.block`, 'Block thread') }
169
184
  </Button>) }
170
- </>}
185
+ </>)}
171
186
  item={item}>
172
187
  <Typography variant="sigma" textColor="neutral600" id="reports-list">
173
188
  { getMessage('page.details.panel.discussion.warnings.reports.list', 'List') }
@@ -179,6 +194,7 @@ const DiscoverTableRow = ({ config, item, onClick }) => {
179
194
  commentId={item.id}
180
195
  items={orderBy(reports, ['resolved', 'createdAt'], ['DESC', 'DESC'])}
181
196
  mutation={resolveReportMutation}
197
+ allowedActions={{ canAccessReports, canReviewReports }}
182
198
  onBlockButtonsStateChange={handleBlockButtonsStateChange}
183
199
  />
184
200
  </ReportsReviewModal>) }
@@ -190,6 +206,11 @@ const DiscoverTableRow = ({ config, item, onClick }) => {
190
206
  DiscoverTableRow.propTypes = {
191
207
  config: PropTypes.object.isRequired,
192
208
  item: PropTypes.object.isRequired,
209
+ allowedActions: PropTypes.shape({
210
+ canModerate: PropTypes.bool,
211
+ canAccessReports: PropTypes.bool,
212
+ canReviewReports: PropTypes.bool,
213
+ }),
193
214
  onClick: PropTypes.func.isRequired,
194
215
  };
195
216
 
@@ -0,0 +1,8 @@
1
+ import styled from 'styled-components';
2
+ import { Link } from '@strapi/design-system/Link';
3
+
4
+ export const TableLink = styled(Link)`
5
+ display: block;
6
+ text-overflow: ellipsis;
7
+ overflow: hidden;
8
+ `
@@ -51,17 +51,20 @@ const Discover = ({ config }) => {
51
51
  const [{ query: queryParams }] = useQueryParams();
52
52
  const _q = queryParams?._q || '';
53
53
 
54
- const viewPermissions = useMemo(() => {
55
- return { view: pluginPermissions.view };
56
- }, []);
54
+ const viewPermissions = useMemo(() => ({
55
+ access: pluginPermissions.access,
56
+ moderate: pluginPermissions.moderate,
57
+ accessReports: pluginPermissions.reports,
58
+ reviewReports: pluginPermissions.reportsReview,
59
+ }), []);
57
60
 
58
61
  const {
59
62
  isLoading: isLoadingForPermissions,
60
- allowedActions: { canView },
63
+ allowedActions: { canAccess, canModerate, canAccessReports, canReviewReports },
61
64
  } = useRBAC(viewPermissions);
62
65
 
63
66
  const { isLoading: isLoadingForData, data: { result, pagination = {} }, isFetching } = useQuery(
64
- ['get-data', queryParams],
67
+ ['get-data', queryParams, canAccess],
65
68
  () => fetchData(queryParams, toggleNotification),
66
69
  {
67
70
  initialData: { },
@@ -86,59 +89,72 @@ const Discover = ({ config }) => {
86
89
  },
87
90
  };
88
91
 
89
-
90
-
91
92
  const emptyContent = _q ? 'search' : 'comments';
92
93
 
93
- const COL_COUNT = 6;
94
-
95
- return <Box background="neutral100">
96
- <Layout sideNav={<Nav />}>
97
- {isLoading || isLoadingForPermissions ? (<LoadingIndicatorPage />) : (
98
- <>
99
- <HeaderLayout title={ getMessage('page.discover.header') } subtitle={ `${total} ${ getMessage('page.discover.header.count')}` } as="h2" />
100
- <ActionLayout
101
- startActions={<>
102
- <SearchURLQuery
103
- label={ getMessage('search.label', 'Search', false) }
104
- />
105
- <TableFilters displayedFilters={filtersSchema} />
106
- </>} />
107
- <ContentLayout>
108
- { !isEmpty(result) ? (<>
109
- <Table colCount={COL_COUNT} rowCount={result.length}>
110
- <Thead>
111
- <Tr>
112
- <Th>
113
- <Typography variant="sigma">{ getMessage('page.discover.table.header.message') }</Typography>
114
- </Th>
115
- <Th>
116
- <Typography variant="sigma">{ getMessage('page.discover.table.header.thread') }</Typography>
117
- </Th>
118
- <Th>
119
- <Typography variant="sigma">{ getMessage('page.discover.table.header.entry') }</Typography>
120
- </Th>
121
- <Th>
122
- <Typography variant="sigma">{ getMessage('page.discover.table.header.lastUpdate') }</Typography>
123
- </Th>
124
- <Th>
125
- <Typography variant="sigma">{ getMessage('page.discover.table.header.status') }</Typography>
126
- </Th>
127
- <Th>
128
- <VisuallyHidden>{ getMessage('page.discover.table.header.actions') }</VisuallyHidden>
129
- </Th>
130
- </Tr>
131
- </Thead>
132
- <Tbody>
133
- {result.map(entry => (<DiscoverTableRow config={config} item={entry} onClick={() => handleClickDisplay(entry.id)}/>))}
134
- </Tbody>
135
- </Table>
136
- <TablePagination pagination={{ pageCount: pagination?.pageCount || 1 }} />
137
- </>) : (<EmptyStateLayout content={emptyLayout[emptyContent]} />) }
138
- </ContentLayout>
139
- </>) }
140
- </Layout>
141
- </Box>;
94
+ const COL_COUNT = 8;
95
+
96
+ if (canAccess) {
97
+ return <Box background="neutral100">
98
+ <Layout>
99
+ {isLoading || isLoadingForPermissions ? (<LoadingIndicatorPage />) : (
100
+ <>
101
+ <HeaderLayout title={ getMessage('page.discover.header') } subtitle={ `${total} ${ getMessage('page.discover.header.count')}` } as="h2" />
102
+ <ActionLayout
103
+ startActions={<>
104
+ <SearchURLQuery
105
+ label={ getMessage('search.label', 'Search', false) }
106
+ />
107
+ <TableFilters displayedFilters={filtersSchema} />
108
+ </>} />
109
+ <ContentLayout>
110
+ { !isEmpty(result) ? (<>
111
+ <Table colCount={COL_COUNT} rowCount={result.length}>
112
+ <Thead>
113
+ <Tr>
114
+ <Th>
115
+ <Typography variant="sigma">{ getMessage('page.discover.table.header.id') }</Typography>
116
+ </Th>
117
+ <Th>
118
+ <Typography variant="sigma">{ getMessage('page.discover.table.header.author') }</Typography>
119
+ </Th>
120
+ <Th>
121
+ <Typography variant="sigma">{ getMessage('page.discover.table.header.message') }</Typography>
122
+ </Th>
123
+ <Th>
124
+ <Typography variant="sigma">{ getMessage('page.discover.table.header.thread') }</Typography>
125
+ </Th>
126
+ <Th>
127
+ <Typography variant="sigma">{ getMessage('page.discover.table.header.entry') }</Typography>
128
+ </Th>
129
+ <Th>
130
+ <Typography variant="sigma">{ getMessage('page.discover.table.header.lastUpdate') }</Typography>
131
+ </Th>
132
+ <Th>
133
+ <Typography variant="sigma">{ getMessage('page.discover.table.header.status') }</Typography>
134
+ </Th>
135
+ <Th>
136
+ <VisuallyHidden>{ getMessage('page.discover.table.header.actions') }</VisuallyHidden>
137
+ </Th>
138
+ </Tr>
139
+ </Thead>
140
+ <Tbody>
141
+ {result.map(entry => (<DiscoverTableRow
142
+ key={`comment-${entry.id}`}
143
+ config={config}
144
+ item={entry}
145
+ allowedActions={{ canModerate, canAccessReports, canReviewReports }}
146
+ onClick={() => handleClickDisplay(entry.id)}
147
+ />))}
148
+ </Tbody>
149
+ </Table>
150
+ <TablePagination pagination={{ pageCount: pagination?.pageCount || 1 }} />
151
+ </>) : (<EmptyStateLayout content={emptyLayout[emptyContent]} />) }
152
+ </ContentLayout>
153
+ </>) }
154
+ </Layout>
155
+ </Box>;
156
+ }
157
+ return null;
142
158
  }
143
159
 
144
160
  const mapStateToProps = makeAppView();