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.
Files changed (75) hide show
  1. package/README.md +421 -72
  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 +5 -1
  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/public/assets/logo.png +0 -0
  36. package/public/assets/preview.png +0 -0
  37. package/server/bootstrap.js +38 -2
  38. package/server/config/index.js +9 -3
  39. package/server/controllers/admin.js +3 -3
  40. package/server/controllers/client.js +4 -18
  41. package/server/controllers/utils/parsers.js +17 -0
  42. package/server/graphql/index.js +26 -0
  43. package/server/graphql/mutations/createAbuseReport.js +20 -0
  44. package/server/graphql/mutations/createComment.js +20 -0
  45. package/server/graphql/mutations/index.js +21 -0
  46. package/server/graphql/mutations/removeComment.js +20 -0
  47. package/server/graphql/mutations/updateComment.js +20 -0
  48. package/server/graphql/queries/findAllFlat.js +29 -0
  49. package/server/graphql/queries/findAllInHierarchy.js +31 -0
  50. package/server/graphql/queries/index.js +19 -0
  51. package/server/graphql/resolvers-config.js +10 -0
  52. package/server/graphql/types/comment-author-user.js +9 -0
  53. package/server/graphql/types/comment-author.js +9 -0
  54. package/server/graphql/types/comment-nested.js +16 -0
  55. package/server/graphql/types/comment.js +16 -0
  56. package/server/graphql/types/create-comment-author.js +9 -0
  57. package/server/graphql/types/create-comment.js +9 -0
  58. package/server/graphql/types/create-report.js +9 -0
  59. package/server/graphql/types/id.js +24 -0
  60. package/server/graphql/types/identify-comment-author.js +6 -0
  61. package/server/graphql/types/index.js +17 -0
  62. package/server/graphql/types/remove-comment.js +8 -0
  63. package/server/graphql/types/report-reason.js +10 -0
  64. package/server/graphql/types/report.js +11 -0
  65. package/server/graphql/types/update-comment.js +9 -0
  66. package/server/register.js +3 -1
  67. package/server/services/admin.js +42 -15
  68. package/server/services/client.js +54 -38
  69. package/server/services/common.js +38 -67
  70. package/server/services/graphql.js +190 -0
  71. package/server/services/index.js +2 -0
  72. package/server/services/utils/functions.js +24 -17
  73. package/server/utils/constants.js +3 -3
  74. package/strapi-server.js +33 -0
  75. package/admin/src/components/EntityDetails/index.js +0 -53
@@ -24,9 +24,9 @@ const ReportsReviewTable = ({
24
24
  commentId,
25
25
  items,
26
26
  mutation,
27
+ allowedActions: { canAccessReports, canReviewReports },
27
28
  onBlockButtonsStateChange,
28
29
  }) => {
29
-
30
30
  const [storedItems, setStoredItems] = useState([]);
31
31
 
32
32
  const { formatDate } = useIntl();
@@ -59,12 +59,12 @@ const ReportsReviewTable = ({
59
59
  };
60
60
 
61
61
  const handleClickResolve = async (reportId) => {
62
+ if (canReviewReports) {
62
63
  lockApp();
63
64
  const item = await mutation.mutateAsync({
64
65
  id: commentId,
65
66
  reportId,
66
67
  });
67
- console.log(item);
68
68
  if (item) {
69
69
  const updatedItems = storedItems.map(_ => ({
70
70
  ..._,
@@ -73,6 +73,7 @@ const ReportsReviewTable = ({
73
73
  setStoredItems(updatedItems);
74
74
  onBlockButtonsStateChange(updatedItems.filter(_ => !_.resolved).length === 0);
75
75
  }
76
+ }
76
77
  }
77
78
 
78
79
  useEffect(() => {
@@ -85,61 +86,68 @@ const ReportsReviewTable = ({
85
86
 
86
87
  const COL_COUNT = 6;
87
88
 
88
- return (<Table colCount={COL_COUNT} rowCount={storedItems.length}>
89
- <Thead>
90
- <Tr>
91
- <Th>
92
- <BaseCheckbox aria-label="Select all entries" />
93
- </Th>
94
- <Th>
95
- <Typography variant="sigma">{ getMessage('page.details.panel.discussion.warnings.reports.dialog.reason') }</Typography>
96
- </Th>
97
- <Th>
98
- <Typography variant="sigma">{ getMessage('page.details.panel.discussion.warnings.reports.dialog.content') }</Typography>
99
- </Th>
100
- <Th>
101
- <Typography variant="sigma">{ getMessage('page.details.panel.discussion.warnings.reports.dialog.createdAt') }</Typography>
102
- </Th>
103
- <Th>
104
- <Typography variant="sigma">{ getMessage('page.details.panel.discussion.warnings.reports.dialog.status') }</Typography>
105
- </Th>
106
- <Th>
107
- <VisuallyHidden>{ getMessage('page.details.panel.discussion.warnings.reports.dialog.actions') }</VisuallyHidden>
108
- </Th>
109
- </Tr>
110
- </Thead>
111
- <Tbody>
112
- {storedItems.map(entry => <Tr key={entry.id}>
113
- <Td>
114
- <BaseCheckbox aria-label={`Select report`} />
115
- </Td>
116
- <Td>
117
- <Typography textColor="neutral800">{renderReason(entry.reason)}</Typography>
118
- </Td>
119
- <Td>
120
- <Typography textColor="neutral800">{entry.content}</Typography>
121
- </Td>
122
- <Td>
123
- <Typography textColor="neutral800">{ formatDate(entry.createdAt, { dateStyle: 'short', timeStyle: 'short' }) }</Typography>
124
- </Td>
125
- <Td>
126
- <Typography textColor="neutral800">{ renderStatus(entry.resolved) }</Typography>
127
- </Td>
128
- <Td>
129
- { !entry.resolved && (<Flex direction="column" alignItems="flex-end">
130
- <Button onClick={() => handleClickResolve(entry.id)} startIcon={<Check />} variant="success">{ getMessage('page.details.panel.discussion.warnings.reports.dialog.actions.resolve', 'resolve') }</Button>
131
- </Flex>) }
132
- </Td>
133
- </Tr>)}
134
- </Tbody>
135
- {/* <TFooter /> */}
136
- </Table>)
89
+ if (canAccessReports) {
90
+ return (<Table colCount={COL_COUNT} rowCount={storedItems.length}>
91
+ <Thead>
92
+ <Tr>
93
+ <Th>
94
+ <BaseCheckbox aria-label="Select all entries" />
95
+ </Th>
96
+ <Th>
97
+ <Typography variant="sigma">{ getMessage('page.details.panel.discussion.warnings.reports.dialog.reason') }</Typography>
98
+ </Th>
99
+ <Th>
100
+ <Typography variant="sigma">{ getMessage('page.details.panel.discussion.warnings.reports.dialog.content') }</Typography>
101
+ </Th>
102
+ <Th>
103
+ <Typography variant="sigma">{ getMessage('page.details.panel.discussion.warnings.reports.dialog.createdAt') }</Typography>
104
+ </Th>
105
+ <Th>
106
+ <Typography variant="sigma">{ getMessage('page.details.panel.discussion.warnings.reports.dialog.status') }</Typography>
107
+ </Th>
108
+ { canReviewReports && (<Th>
109
+ <VisuallyHidden>{ getMessage('page.details.panel.discussion.warnings.reports.dialog.actions') }</VisuallyHidden>
110
+ </Th>)}
111
+ </Tr>
112
+ </Thead>
113
+ <Tbody>
114
+ {storedItems.map(entry => <Tr key={entry.id}>
115
+ <Td>
116
+ <BaseCheckbox aria-label={`Select report`} />
117
+ </Td>
118
+ <Td>
119
+ <Typography textColor="neutral800">{renderReason(entry.reason)}</Typography>
120
+ </Td>
121
+ <Td>
122
+ <Typography textColor="neutral800">{entry.content}</Typography>
123
+ </Td>
124
+ <Td>
125
+ <Typography textColor="neutral800">{ formatDate(entry.createdAt, { dateStyle: 'short', timeStyle: 'short' }) }</Typography>
126
+ </Td>
127
+ <Td>
128
+ <Typography textColor="neutral800">{ renderStatus(entry.resolved) }</Typography>
129
+ </Td>
130
+ { canReviewReports && (<Td>
131
+ { !entry.resolved && (<Flex direction="column" alignItems="flex-end">
132
+ <Button onClick={() => handleClickResolve(entry.id)} startIcon={<Check />} variant="success">{ getMessage('page.details.panel.discussion.warnings.reports.dialog.actions.resolve', 'resolve') }</Button>
133
+ </Flex>) }
134
+ </Td>)}
135
+ </Tr>)}
136
+ </Tbody>
137
+ {/* <TFooter /> */}
138
+ </Table>);
139
+ }
140
+ return null;
137
141
  };
138
142
 
139
143
  ReportsReviewTable.propTypes = {
140
- commentId: PropTypes.string.isRequired,
144
+ commentId: PropTypes.oneOfType(PropTypes.string, PropTypes.number).isRequired,
141
145
  items: PropTypes.array.isRequired,
142
146
  mutation: PropTypes.func.isRequired,
147
+ allowedActions: PropTypes.shape({
148
+ canAccessReports: PropTypes.bool,
149
+ canReviewReports: PropTypes.bool,
150
+ }),
143
151
  onBlockButtonsStateChange: PropTypes.func.isRequired,
144
152
  };
145
153
 
@@ -1,7 +1,7 @@
1
1
 
2
2
  import React from 'react';
3
3
 
4
- const initSize = 24;
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 strokeWidth="1" d="M5.5,21 C7.98528137,21 10,18.9852814 10,16.5 C10,14.0147186 7.98528137,12 5.5,12 C3.01471863,12 1,14.0147186 1,16.5 C1,18.9852814 3.01471863,21 5.5,21 Z M1,16 L1,7 L1,6.5 C1,4.01471863 3.01471863,2 5.5,2 L6,2 M23,16 L23,7 L23,6.5 C23,4.01471863 20.9852814,2 18.5,2 L18,2 M18.5,21 C20.9852814,21 23,18.9852814 23,16.5 C23,14.0147186 20.9852814,12 18.5,12 C16.0147186,12 14,14.0147186 14,16.5 C14,18.9852814 16.0147186,21 18.5,21 Z M10,17 C10,17 10,15 12,15 C14,15 14,17 14,17"/>
14
+ <path fillRule="evenodd" clipRule="evenodd" d="M0 4a4 4 0 014-4h8a4 4 0 014 4v8a4 4 0 01-4 4H4a4 4 0 01-4-4V4zm6.996.165a1.017 1.017 0 112.012 0L8 11 6.996 4.165zM8 11a1 1 0 110 2 1 1 0 010-2z" />
15
15
  </g>
16
16
  </svg>
17
17
 
@@ -23,7 +23,7 @@ export default {
23
23
 
24
24
  return component;
25
25
  },
26
- permissions: pluginPermissions,
26
+ permissions: pluginPermissions.access,
27
27
  });
28
28
  app.addReducers(reducers);
29
29
  app.registerPlugin({
@@ -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));