strapi-plugin-comments 2.0.0-beta.5 → 2.0.0-beta.6

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 CHANGED
@@ -58,7 +58,8 @@ In our minimum support we're following [official Node.js releases timelines](htt
58
58
 
59
59
  **Supported Strapi versions**:
60
60
 
61
- - Strapi v4.0.4 (recently tested)
61
+ - Strapi v4.0.5 (recently tested)
62
+ - Strapi v4.x
62
63
 
63
64
  (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.)
64
65
 
@@ -101,6 +102,20 @@ To setup amend default plugin configuration we recommend to put following snippe
101
102
 
102
103
  > To be done
103
104
 
105
+ ## RBAC
106
+ Plugin provides granular permissions based on Strapi RBAC functionality.
107
+
108
+ ### Mandatory permissions
109
+ For any role different than **Super Admin**, to access the **Comments panel** you must set following permissions:
110
+ - _Plugins_ -> _Content-type-builder_ -> _Read_ - gives you ability to fetch Content Type schema
111
+ - _Plugins_ -> _Comments_ -> _Comments: Read_ - gives you the basic read access to **Comments Panel**
112
+
113
+ ### Optional permissions
114
+ Feature / Capability focused permissions:
115
+ - _Plugins_ -> _Comments_ -> _Comments: Moderate_ - allows you to block, unblock, approve & reject comments
116
+ - _Plugins_ -> _Comments_ -> _Reports: Read_ - allows you to see the list of issued abuse reports against comments
117
+ - _Plugins_ -> _Comments_ -> _Reports: Moderate_ - allows you to review (resolve) issued abuse reports against comments
118
+
104
119
  ## Public API Comment model
105
120
 
106
121
  ### Generic (non Strapi User)
@@ -18,7 +18,7 @@ import { getMessage, getUrl } from '../../utils';
18
18
  import LoadingIndicatorOverlay from '../LoadingIndicatorOverlay';
19
19
  import { LoadingIndicatorContainer } from '../LoadingIndicatorOverlay/styles';
20
20
 
21
- const DiscussionThread = ({ level = [], selected = {}, isReloading }) => {
21
+ const DiscussionThread = ({ level = [], selected = {}, allowedActions, isReloading }) => {
22
22
  const rootThread = selected?.threadOf;
23
23
  return (<LoadingIndicatorContainer as={Box} padding={4}>
24
24
  { isReloading && <LoadingIndicatorOverlay />}
@@ -34,13 +34,14 @@ const DiscussionThread = ({ level = [], selected = {}, isReloading }) => {
34
34
  <Divider />
35
35
  </Box>
36
36
  <Flex as="ul" direction="column" alignItems="flex-start">
37
- { rootThread && (<DiscussionThreadItem {...rootThread} isThreadAuthor root pinned />) }
37
+ { rootThread && (<DiscussionThreadItem {...rootThread} allowedActions={allowedActions} isThreadAuthor root pinned />) }
38
38
  { level.map(item => {
39
39
  const isSelected = selected.id === item.id;
40
40
  const isThreadAuthor = !isNil(selected?.threadOf?.authorId) && (selected?.threadOf?.authorId === item.authorId);
41
41
  return (<DiscussionThreadItem
42
42
  key={`comment-${item.id}`}
43
43
  {...item}
44
+ allowedActions={allowedActions}
44
45
  root={isNil(rootThread)}
45
46
  blockedThread={rootThread?.blockedThread || item.blockedThread}
46
47
  isSelected={isSelected}
@@ -54,6 +55,11 @@ const DiscussionThread = ({ level = [], selected = {}, isReloading }) => {
54
55
  DiscussionThread.propTypes = {
55
56
  level: PropTypes.array.isRequired,
56
57
  selected: PropTypes.object.isRequired,
58
+ allowedActions: PropTypes.shape({
59
+ canModerate: PropTypes.bool,
60
+ canAccessReports: PropTypes.bool,
61
+ canReviewReports: PropTypes.bool,
62
+ }),
57
63
  isReloading: PropTypes.bool,
58
64
  };
59
65
 
@@ -20,8 +20,22 @@ import DiscussionThreadItemActions from '../DiscussionThreadItemActions';
20
20
  import { getMessage, getUrl } from '../../utils';
21
21
  import DiscussionThreadItemWarnings from '../DiscussionThreadItemWarnings';
22
22
 
23
- const DiscussionThreadItem = (item) => {
24
- const { id, isSelected, isThreadAuthor, content, root, preview, pinned, blocked, removed, blockedThread, gotThread, threadFirstItemId, authorUser, authorAvatar, authorName } = item;
23
+ const DiscussionThreadItem = (props) => {
24
+ const {
25
+ isSelected,
26
+ isThreadAuthor,
27
+ content,
28
+ root,
29
+ preview,
30
+ pinned,
31
+ blocked,
32
+ removed,
33
+ blockedThread,
34
+ gotThread,
35
+ threadFirstItemId,
36
+ authorUser,
37
+ authorAvatar,
38
+ authorName } = props;
25
39
 
26
40
  let background = 'neutral100';
27
41
  let borderColor = 'neutral200';
@@ -62,12 +76,12 @@ const DiscussionThreadItem = (item) => {
62
76
  <Box as={Flex} grow={1} alignItems="center">
63
77
  <Typography variant="omega">{ content }</Typography>
64
78
  </Box>
65
- { !preview && (<DiscussionThreadItemActions { ...item } root={root || pinned} />) }
79
+ { !preview && (<DiscussionThreadItemActions { ...props } root={root || pinned} />) }
66
80
  </DiscussionThreadItemBoxContent>
67
- { !(preview || isBlocked || removed) && (<DiscussionThreadItemWarnings { ...item } gotThread={gotThread || root} />) }
81
+ { !(preview || isBlocked || removed) && (<DiscussionThreadItemWarnings { ...props } gotThread={gotThread || root} />) }
68
82
  </Flex>
69
83
  <Divider background={dividerColor} />
70
- <DiscussionThreadItemMeta { ...item } />
84
+ <DiscussionThreadItemMeta { ...props } />
71
85
  </DiscussionThreadItemBox>
72
86
  </DiscussionThreadItemContent>
73
87
 
@@ -81,6 +95,11 @@ const DiscussionThreadItem = (item) => {
81
95
 
82
96
  DiscussionThreadItem.propTypes = {
83
97
  content: PropTypes.string.isRequired,
98
+ allowedActions: PropTypes.shape({
99
+ canModerate: PropTypes.bool,
100
+ canAccessReports: PropTypes.bool,
101
+ canReviewReports: PropTypes.bool,
102
+ }),
84
103
  };
85
104
 
86
105
  export default DiscussionThreadItem;
@@ -7,7 +7,7 @@
7
7
  import React, { useState } from 'react';
8
8
  import PropTypes from 'prop-types';
9
9
  import { useMutation, useQueryClient } from 'react-query';
10
- import { isNil } from 'lodash';
10
+ import { isNil, isEmpty } from 'lodash';
11
11
  import { Button } from '@strapi/design-system/Button';
12
12
  import { Flex } from '@strapi/design-system/Flex';
13
13
  import { IconButton } from '@strapi/design-system/IconButton';
@@ -20,7 +20,7 @@ import pluginId from '../../pluginId';
20
20
  import { LockIcon, UnlockIcon } from '../icons';
21
21
  import DiscussionThreadItemApprovalFlowActions from '../DiscussionThreadItemApprovalFlowActions';
22
22
 
23
- const DiscussionThreadItemActions = ({ id, blocked, removed, blockedThread, gotThread, pinned, approvalStatus, root }) => {
23
+ const DiscussionThreadItemActions = ({ id, blocked, removed, blockedThread, gotThread, pinned, reports = [], approvalStatus, root, allowedActions: { canModerate, canAccessReports, canReviewReports } }) => {
24
24
 
25
25
  const [blockConfirmationVisible, setBlockConfirmationVisible] = useState(false);
26
26
  const [blockThreadConfirmationVisible, setBlockThreadConfirmationVisible] = useState(false);
@@ -72,9 +72,12 @@ const DiscussionThreadItemActions = ({ id, blocked, removed, blockedThread, gotT
72
72
  const needsApproval = gotApprovalFlow && (approvalStatus === 'PENDING');
73
73
  const isBlocked = blocked || blockedThread;
74
74
  const isRejected = gotApprovalFlow && (approvalStatus === 'REJECTED');
75
+ const openReports = reports?.filter(_ => !_.resolved);
76
+ const hasReports = !isEmpty(openReports);
77
+ const reviewFlowEnabled = (canAccessReports || canReviewReports) && hasReports;
75
78
 
76
79
  const resolveStatusBadge = () => {
77
- const status = resolveCommentStatus({ removed, blocked, blockedThread, approvalStatus });
80
+ const status = resolveCommentStatus({ removed, blocked, blockedThread, approvalStatus, reviewFlowEnabled: !isBlocked && reviewFlowEnabled });
78
81
  return {
79
82
  badgeVisible: isBlocked || gotApprovalFlow || removed,
80
83
  badgeColor: resolveCommentStatusColor(status),
@@ -86,44 +89,68 @@ const DiscussionThreadItemActions = ({ id, blocked, removed, blockedThread, gotT
86
89
 
87
90
  const handleBlockClick = () => setBlockConfirmationVisible(true);
88
91
  const handleBlockConfirm = async () => {
89
- lockApp();
90
- blockItemMutation.mutate(id);
92
+ if (canModerate) {
93
+ lockApp();
94
+ blockItemMutation.mutate(id);
95
+ }
91
96
  };
92
97
  const handleBlockCancel = () => {
93
98
  setBlockConfirmationVisible(false);
94
99
  };
95
100
  const handleUnblockClick = async () => {
96
- lockApp();
97
- unblockItemMutation.mutate(id);
101
+ if (canModerate) {
102
+ lockApp();
103
+ unblockItemMutation.mutate(id);
104
+ }
98
105
  };
99
106
 
100
107
  const handleBlockThreadClick = () => setBlockThreadConfirmationVisible(true);
101
108
  const handleBlockThreadConfirm = async () => {
102
- lockApp();
103
- blockItemThreadMutation.mutate(id);
109
+ if (canModerate) {
110
+ lockApp();
111
+ blockItemThreadMutation.mutate(id);
112
+ }
104
113
  };
105
114
  const handleBlockThreadCancel = () => {
106
115
  setBlockThreadConfirmationVisible(false);
107
116
  };
108
117
  const handleUnblockThreadClick = async () => {
109
- lockApp();
110
- unblockItemThreadMutation.mutate(id);
118
+ if (canModerate) {
119
+ lockApp();
120
+ unblockItemThreadMutation.mutate(id);
121
+ }
111
122
  };
112
123
 
113
- if (removed || isRejected) {
124
+ if (removed || isRejected || !canModerate) {
114
125
  return (<DiscussionThreadItemActionsWrapper as={Flex} direction="row">
115
- <DiscussionThreadItemActionsBadge color={badgeColor} backgroundColor={`${badgeColor}100`} textColor={`${badgeColor}600`}>{getMessage(`page.common.item.status.${badgeLabel}`, badgeLabel)}</DiscussionThreadItemActionsBadge>
126
+ <DiscussionThreadItemActionsBadge
127
+ color={badgeColor}
128
+ backgroundColor={`${badgeColor}100`}
129
+ textColor={`${badgeColor}600`}>
130
+ { getMessage({
131
+ id: `page.common.item.status.${badgeLabel}`,
132
+ props: { count: openReports.length },
133
+ }, badgeLabel)}
134
+ </DiscussionThreadItemActionsBadge>
116
135
  </DiscussionThreadItemActionsWrapper>);
117
136
  }
118
137
 
119
138
  return (<>
120
139
  <DiscussionThreadItemActionsWrapper as={Flex} direction="row">
121
- { badgeVisible && (<DiscussionThreadItemActionsBadge color={badgeColor} backgroundColor={`${badgeColor}100`} textColor={`${badgeColor}600`}>{getMessage(`page.common.item.status.${badgeLabel}`, badgeLabel)}</DiscussionThreadItemActionsBadge>)}
140
+ { badgeVisible && (<DiscussionThreadItemActionsBadge
141
+ color={badgeColor}
142
+ backgroundColor={`${badgeColor}100`}
143
+ textColor={`${badgeColor}600`}>
144
+ { getMessage({
145
+ id: `page.common.item.status.${badgeLabel}`,
146
+ props: { count: openReports.length },
147
+ }, badgeLabel)}
148
+ </DiscussionThreadItemActionsBadge>)}
122
149
  { !blockedThread && !(blocked || needsApproval) && (<IconButton onClick={handleBlockClick} loading={blockItemMutation.isLoading} icon={<LockIcon />} label={getMessage('page.details.actions.comment.block', 'Block')} style={(!blockedThread && root) ? { marginTop: '1px', marginRight: '.5rem' } : {}} />)}
123
150
  { !blockedThread && blocked && (<IconButton onClick={handleUnblockClick} loading={unblockItemMutation.isLoading} icon={<UnlockIcon />} label={getMessage('page.details.actions.comment.unblock', 'Unblock')} style={(blocked && !blockedThread) ? { marginTop: '1px', marginRight: '.5rem' } : {}} />)}
124
151
  { (!blockedThread && (gotThread || pinned)) && (<Button onClick={handleBlockThreadClick} startIcon={<LockIcon />} loading={blockItemThreadMutation.isLoading} variant="danger">{ getMessage('page.details.actions.thread.block', 'Block thread') }</Button>) }
125
152
  { (blockedThread && (gotThread || pinned)) && (<Button onClick={handleUnblockThreadClick} startIcon={<UnlockIcon />} loading={unblockItemThreadMutation.isLoading} variant="success">{ getMessage('page.details.actions.thread.unblock', 'Unblock thread') }</Button>) }
126
- { needsApproval && (<DiscussionThreadItemApprovalFlowActions id={id} queryToInvalidate="get-details-data" />) }
153
+ { needsApproval && (<DiscussionThreadItemApprovalFlowActions id={id} allowedActions={{ canModerate }} queryToInvalidate="get-details-data" />) }
127
154
  </DiscussionThreadItemActionsWrapper>
128
155
  { !blocked && (<ConfirmationDialog
129
156
  isVisible={blockConfirmationVisible}
@@ -153,9 +180,9 @@ DiscussionThreadItemActions.propTypes = {
153
180
  blocked: PropTypes.bool.isRequired,
154
181
  blockedThread: PropTypes.bool.isRequired,
155
182
  approvalStatus: PropTypes.oneOfType([PropTypes.nullable, PropTypes.oneOf(['PENDING', 'APPROVED', 'REJECTED'])]),
156
- onBlockClick: PropTypes.func.isRequired,
157
- onUnBlockClick: PropTypes.func.isRequired,
158
- onApprovalGroupClick: PropTypes.func.isRequired,
183
+ allowedActions: PropTypes.shape({
184
+ canModerate: PropTypes.bool,
185
+ }),
159
186
  };
160
187
 
161
188
  export default DiscussionThreadItemActions;
@@ -15,13 +15,13 @@ import { DiscussionThreadItemActionsGroupWrapper } from './styles';
15
15
  import { approveItem, rejectItem } from '../../pages/utils/api';
16
16
  import pluginId from '../../pluginId';
17
17
 
18
- const DiscussionThreadItemApprovalFlowActions = ({ id, queryToInvalidate }) => {
18
+ const DiscussionThreadItemApprovalFlowActions = ({ id, allowedActions: { canModerate }, queryToInvalidate }) => {
19
19
 
20
20
  const toggleNotification = useNotification();
21
21
  const queryClient = useQueryClient();
22
22
  const { lockApp, unlockApp } = useOverlayBlocker();
23
23
 
24
- const onSuccess = (message, stateAction = () => {}) => async () => {
24
+ const onSuccess = (message) => async () => {
25
25
  if (queryToInvalidate) {
26
26
  await queryClient.invalidateQueries(queryToInvalidate);
27
27
  }
@@ -45,26 +45,35 @@ const DiscussionThreadItemApprovalFlowActions = ({ id, queryToInvalidate }) => {
45
45
  refetchActive: false,
46
46
  });
47
47
 
48
-
49
48
  const handleApproveClick = () => {
50
- lockApp();
51
- approveItemMutation.mutate(id);
49
+ if (canModerate) {
50
+ lockApp();
51
+ approveItemMutation.mutate(id);
52
+ }
52
53
  };
53
54
 
54
55
  const handleRejectClick = () => {
55
- lockApp();
56
- rejectItemMutation.mutate(id);
56
+ if (canModerate) {
57
+ lockApp();
58
+ rejectItemMutation.mutate(id);
59
+ }
57
60
  };
58
61
 
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>);
62
+ if (canModerate) {
63
+ return (<DiscussionThreadItemActionsGroupWrapper>
64
+ <IconButton icon={<Check />} label={getMessage('page.details.actions.comment.approve', 'Approve')} onClick={handleApproveClick} />
65
+ <IconButton icon={<Cross />} label={getMessage('page.details.actions.comment.reject', 'Reject')} onClick={handleRejectClick} />
66
+ </DiscussionThreadItemActionsGroupWrapper>);
67
+ }
68
+ return null;
63
69
  };
64
70
 
65
71
  DiscussionThreadItemApprovalFlowActions.propTypes = {
66
72
  id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
67
73
  queryToInvalidate: PropTypes.string,
74
+ allowedActions: PropTypes.shape({
75
+ canModerate: PropTypes.bool,
76
+ }),
68
77
  wrapped: PropTypes.bool,
69
78
  };
70
79
 
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import React, { useState } from 'react';
8
+ import PropTypes from 'prop-types';
8
9
  import { useMutation, useQueryClient } from 'react-query';
9
10
  import { isEmpty, orderBy } from 'lodash';
10
11
  import { Badge } from '@strapi/design-system/Badge';
@@ -23,7 +24,7 @@ import pluginId from '../../pluginId';
23
24
  import { LockIcon, ReviewIcon } from '../icons';
24
25
 
25
26
  const DiscussionThreadItemWarnings = (item) => {
26
- const { id, reports, gotThread } = item;
27
+ const { id, reports, gotThread, allowedActions: { canModerate, canAccessReports, canReviewReports }} = item;
27
28
 
28
29
  const [reportsReviewVisible, setReportsReviewVisible] = useState(false);
29
30
  const [blockButtonsDisabled, setBlockButtonsDisabled] = useState(false);
@@ -93,7 +94,7 @@ const DiscussionThreadItemWarnings = (item) => {
93
94
  }
94
95
 
95
96
  return (<>
96
- <DiscussionThreadItemWarningsWrapper as={Flex} direction="row" padding={2} background="danger100" borderColor="danger200" border={1} hasRadius>
97
+ { canAccessReports && (<DiscussionThreadItemWarningsWrapper as={Flex} direction="row" padding={2} background="danger100" borderColor="danger200" border={1} hasRadius>
97
98
  <Box as={Flex} paddingRight={2} direction="row" grow={1}>
98
99
  <Badge textColor="neutral0" backgroundColor="warning600">{ openReports.length }</Badge>
99
100
  <Box paddingLeft={2}>
@@ -103,19 +104,19 @@ const DiscussionThreadItemWarnings = (item) => {
103
104
  <Button onClick={handleReportsReviewClick} variant="ghost" endIcon={<ReviewIcon />}>
104
105
  { getMessage(`page.details.actions.comment.report.review`, 'review') }
105
106
  </Button>
106
- </DiscussionThreadItemWarningsWrapper>
107
- <ReportsReviewModal
107
+ </DiscussionThreadItemWarningsWrapper>) }
108
+ { (canAccessReports || canReviewReports) && (<ReportsReviewModal
108
109
  isVisible={reportsReviewVisible}
109
110
  isActionAsync={isLoading}
110
111
  onClose={handleReportsReviewClose}
111
- startActions={<>
112
+ startActions={canModerate && (<>
112
113
  <Button onClick={handleBlockItemClick} variant="danger-light" startIcon={<LockIcon />} disabled={blockButtonsDisabled}>
113
114
  { getMessage(`page.details.actions.comment.block`, 'Block comment') }
114
115
  </Button>
115
116
  { gotThread && (<Button onClick={handleBlockItemThreadClick} variant="danger" startIcon={<LockIcon />} disabled={blockButtonsDisabled}>
116
117
  { getMessage(`page.details.actions.thread.block`, 'Block thread') }
117
118
  </Button>) }
118
- </>}
119
+ </>)}
119
120
  item={item}>
120
121
  <Typography variant="sigma" textColor="neutral600" id="reports-list">
121
122
  { getMessage('page.details.panel.discussion.warnings.reports.list', 'List') }
@@ -124,17 +125,22 @@ const DiscussionThreadItemWarnings = (item) => {
124
125
  <Divider />
125
126
  </Box>
126
127
  <ReportsReviewTable
127
- commentId={item.id}
128
+ commentId={id}
128
129
  items={orderBy(reports, ['resolved', 'createdAt'], ['DESC', 'DESC'])}
129
130
  mutation={resolveReportMutation}
131
+ allowedActions={{ canAccessReports, canReviewReports }}
130
132
  onBlockButtonsStateChange={handleBlockButtonsStateChange}
131
133
  />
132
- </ReportsReviewModal>
134
+ </ReportsReviewModal>) }
133
135
  </>);
134
136
  };
135
137
 
136
138
  DiscussionThreadItemWarnings.propTypes = {
137
-
139
+ allowedActions: PropTypes.shape({
140
+ canModerate: PropTypes.bool,
141
+ canAccessReports: PropTypes.bool,
142
+ canReviewReports: PropTypes.bool,
143
+ }),
138
144
  };
139
145
 
140
146
  export default DiscussionThreadItemWarnings;
@@ -37,6 +37,11 @@ const ReportsReviewModal = ({
37
37
  endActions: PropTypes.array,
38
38
  item: PropTypes.object.isRequired,
39
39
  children: PropTypes.array.isRequired,
40
+ allowedActions: PropTypes.shape({
41
+ canModerate: PropTypes.bool,
42
+ canAccessReports: PropTypes.bool,
43
+ canReviewReports: PropTypes.bool,
44
+ }),
40
45
  onClose: PropTypes.func.isRequired,
41
46
  };
42
47
 
@@ -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
 
@@ -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 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" />
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({
@@ -30,7 +30,7 @@ const DetailsEntity = ({ data = {}, schema = {}, config = {} }) => {
30
30
  FIELDS_LIMIT
31
31
  );
32
32
 
33
- const formatLabel = label => label.split('_')
33
+ const formatLabel = (label = '') => label.split('_')
34
34
  .map(_ => capitalize(_))
35
35
  .join(' ');
36
36
 
@@ -47,7 +47,7 @@ const DetailsEntity = ({ data = {}, schema = {}, config = {} }) => {
47
47
  <Typography>{ data[entityLabelKey] }</Typography>
48
48
  </Flex>
49
49
  { itemKeys.map(_ =>
50
- (<Flex direction="column" alignItems="flex-start">
50
+ (<Flex key={`prop_${_}`} direction="column" alignItems="flex-start">
51
51
  <Typography fontWeight="bold">{ formatLabel(_) }</Typography>
52
52
  <Typography>{ data[_] }</Typography>
53
53
  </Flex>))
@@ -20,6 +20,7 @@ const DetailsFilters = ({ data, onChange }) => {
20
20
  <Stack size={2} horizontal>
21
21
  <Typography variant="epsilon">{getMessage('page.details.filters.label', '')}</Typography>
22
22
  <Switch
23
+ label={getMessage('page.details.filters.removed.label', 'Comments visibility')}
23
24
  onLabel={getMessage('page.details.filters.removed.show', 'Show')}
24
25
  offLabel={getMessage('page.details.filters.removed.hide', 'Hide')}
25
26
  selected={removed} onChange={() => handleOnChange('removed', !removed)}
@@ -50,13 +50,16 @@ const Details = ({ config }) => {
50
50
  const trackUsageRef = useRef(trackUsage);
51
51
  const toggleNotification = useNotification();
52
52
 
53
- const viewPermissions = useMemo(() => {
54
- return { view: pluginPermissions.view };
55
- }, []);
53
+ const viewPermissions = useMemo(() => ({
54
+ access: pluginPermissions.access,
55
+ moderate: pluginPermissions.moderate,
56
+ accessReports: pluginPermissions.reports,
57
+ reviewReports: pluginPermissions.reportsReview,
58
+ }), []);
56
59
 
57
60
  const {
58
61
  isLoading: isLoadingForPermissions,
59
- allowedActions: { canView },
62
+ allowedActions: { canAccess, canModerate, canAccessReports, canReviewReports },
60
63
  } = useRBAC(viewPermissions);
61
64
 
62
65
  const [filters, setFilters] = useState({});
@@ -64,7 +67,7 @@ const Details = ({ config }) => {
64
67
  const regexUID = new RegExp(config.regex.uid);
65
68
 
66
69
  const { isLoading: isLoadingForData, data, isFetching } = useQuery(
67
- ['get-details-data', id, filters],
70
+ ['get-details-data', id, filters, canAccess],
68
71
  () => fetchDetailsData(id, filters, toggleNotification),
69
72
  {
70
73
  initialData: { },
@@ -75,7 +78,7 @@ const Details = ({ config }) => {
75
78
  const entityUidValid = entity?.uid && regexUID.test(entity.uid);
76
79
 
77
80
  const { data: contentTypeData } = useQuery(
78
- ['get-additional-data', entity?.uid],
81
+ ['get-additional-data', entity?.uid, canAccess],
79
82
  () => fetchContentTypeData(entity?.uid, toggleNotification),
80
83
  {
81
84
  enabled: !!entity?.uid,
@@ -87,37 +90,40 @@ const Details = ({ config }) => {
87
90
 
88
91
  const isLoading = isLoadingForData || isFetching;
89
92
 
90
- return <Box background="neutral100">
91
- <Layout>
92
- {(isLoading || isLoadingForPermissions) && isEmpty(data) ? (<LoadingIndicatorPage />) : (
93
- <>
94
- <HeaderLayout
95
- navigationAction={
96
- <Link startIcon={<ArrowLeft />} to={getUrl(`discover`)}>
97
- { getMessage('HeaderLayout.link.go-back', 'Back', false) }
98
- </Link>}
99
- title={ getMessage('page.details.header') }
100
- subtitle={ getMessage('page.details.header.description') }
101
- primaryAction={<DetailsFilters data={filters} onChange={handleChangeFilters} />}
102
- secondaryAction={<Box as={Flex}>&nbsp;</Box>}
103
- as="h2" />
104
- <ContentLayout>
105
- <TwoColsLayout
106
- startCol={<DiscussionThread level={level} selected={selected} isReloading={isLoading} />}
107
- endCol={ <Stack size={2}>
108
- <DiscussionStatus item={selected} />
109
- { !contentTypeData?.schema ? (<Box padding={4}>
110
- <Loader small>{ getMessage('page.details.panel.loading', 'Loading...') }</Loader>
111
- </Box>) :
112
- (<DetailsEntity data={entity} schema={contentTypeData?.schema} config={config} />)
113
- }
114
- </Stack>
115
- }
116
- />
117
- </ContentLayout>
118
- </>) }
119
- </Layout>
120
- </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;
121
127
  }
122
128
 
123
129
  const mapStateToProps = makeAppView();
@@ -25,7 +25,7 @@ import { TableLink } from './styles';
25
25
  import renderEntryTitle from '../../../../utils/renderEntryTitle';
26
26
  import DiscussionThreadItemApprovalFlowActions from '../../../../components/DiscussionThreadItemApprovalFlowActions';
27
27
 
28
- const DiscoverTableRow = ({ config, item, onClick }) => {
28
+ const DiscoverTableRow = ({ config, item, allowedActions: { canModerate, canAccessReports, canReviewReports }, onClick }) => {
29
29
 
30
30
  const { id, reports } = item;
31
31
 
@@ -74,14 +74,22 @@ const DiscoverTableRow = ({ config, item, onClick }) => {
74
74
  refetchActive: false,
75
75
  });
76
76
 
77
- const handleReportsReviewClick = () => setReportsReviewVisible(true);
77
+ const handleReportsReviewClick = () => {
78
+ if (canAccessReports) {
79
+ setReportsReviewVisible(true);
80
+ }
81
+ };
78
82
  const handleBlockItemClick = async () => {
79
- lockApp();
80
- blockItemMutation.mutate(id);
83
+ if (canModerate) {
84
+ lockApp();
85
+ blockItemMutation.mutate(id);
86
+ }
81
87
  };
82
88
  const handleBlockItemThreadClick = async () => {
83
- lockApp();
84
- blockItemThreadMutation.mutate(id);
89
+ if (canModerate) {
90
+ lockApp();
91
+ blockItemThreadMutation.mutate(id);
92
+ }
85
93
  };
86
94
  const handleReportsReviewClose = async () => {
87
95
  await queryClient.invalidateQueries('get-data');
@@ -91,7 +99,7 @@ const DiscoverTableRow = ({ config, item, onClick }) => {
91
99
  const isLoading = blockItemMutation.isLoading || blockItemThreadMutation.isLoading || resolveReportMutation.isLoading;
92
100
  const openReports = reports?.filter(_ => !_.resolved);
93
101
  const hasReports = !isEmpty(openReports);
94
- const reviewFlowEnabled = hasReports && !(item.blocked || item.blockedThread);
102
+ const reviewFlowEnabled = (canAccessReports && hasReports) && !(item.blocked || item.blockedThread);
95
103
 
96
104
  const handleClick = () => onClick();
97
105
  const handleBlockButtonsStateChange = disabled => setBlockButtonsDisabled(disabled);
@@ -146,22 +154,28 @@ const DiscoverTableRow = ({ config, item, onClick }) => {
146
154
  <Flex direction="column" alignItems="flex-end">
147
155
  <Stack size={actionItemsCount} horizontal>
148
156
  { reviewFlowEnabled && (<IconButton onClick={handleReportsReviewClick} label={ getMessage('page.discover.table.reports.review') } icon={<ReviewIcon />} />)}
149
- { needsApproval && (<DiscussionThreadItemApprovalFlowActions id={item.id} queryToInvalidate="get-data" />)}
157
+ { (canModerate && needsApproval) && (
158
+ <DiscussionThreadItemApprovalFlowActions
159
+ id={item.id}
160
+ allowedActions={{ canModerate }}
161
+ queryToInvalidate="get-data" />
162
+ )}
150
163
  <IconButton onClick={handleClick} label={ getMessage('page.discover.table.action.display') } icon={<Eye />} />
151
164
  </Stack>
152
165
  </Flex>
153
166
  { reviewFlowEnabled && (<ReportsReviewModal
154
167
  isVisible={reportsReviewVisible}
155
168
  isActionAsync={isLoading}
169
+ allowedActions={{ canModerate, canAccessReports, canReviewReports }}
156
170
  onClose={handleReportsReviewClose}
157
- startActions={<>
171
+ startActions={canModerate && (<>
158
172
  <Button onClick={handleBlockItemClick} variant="danger-light" startIcon={<LockIcon />} disabled={blockButtonsDisabled}>
159
173
  { getMessage(`page.details.actions.comment.block`, 'Block comment') }
160
174
  </Button>
161
175
  { item.gotThread && (<Button onClick={handleBlockItemThreadClick} variant="danger" startIcon={<LockIcon />} disabled={blockButtonsDisabled}>
162
176
  { getMessage(`page.details.actions.thread.block`, 'Block thread') }
163
177
  </Button>) }
164
- </>}
178
+ </>)}
165
179
  item={item}>
166
180
  <Typography variant="sigma" textColor="neutral600" id="reports-list">
167
181
  { getMessage('page.details.panel.discussion.warnings.reports.list', 'List') }
@@ -173,6 +187,7 @@ const DiscoverTableRow = ({ config, item, onClick }) => {
173
187
  commentId={item.id}
174
188
  items={orderBy(reports, ['resolved', 'createdAt'], ['DESC', 'DESC'])}
175
189
  mutation={resolveReportMutation}
190
+ allowedActions={{ canAccessReports, canReviewReports }}
176
191
  onBlockButtonsStateChange={handleBlockButtonsStateChange}
177
192
  />
178
193
  </ReportsReviewModal>) }
@@ -184,6 +199,11 @@ const DiscoverTableRow = ({ config, item, onClick }) => {
184
199
  DiscoverTableRow.propTypes = {
185
200
  config: PropTypes.object.isRequired,
186
201
  item: PropTypes.object.isRequired,
202
+ allowedActions: PropTypes.shape({
203
+ canModerate: PropTypes.bool,
204
+ canAccessReports: PropTypes.bool,
205
+ canReviewReports: PropTypes.bool,
206
+ }),
187
207
  onClick: PropTypes.func.isRequired,
188
208
  };
189
209
 
@@ -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,62 +89,69 @@ const Discover = ({ config }) => {
86
89
  },
87
90
  };
88
91
 
89
-
90
-
91
92
  const emptyContent = _q ? 'search' : 'comments';
92
93
 
93
94
  const COL_COUNT = 7;
94
95
 
95
- return <Box background="neutral100">
96
- <Layout>
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.id') }</Typography>
114
- </Th>
115
- <Th>
116
- <Typography variant="sigma">{ getMessage('page.discover.table.header.message') }</Typography>
117
- </Th>
118
- <Th>
119
- <Typography variant="sigma">{ getMessage('page.discover.table.header.thread') }</Typography>
120
- </Th>
121
- <Th>
122
- <Typography variant="sigma">{ getMessage('page.discover.table.header.entry') }</Typography>
123
- </Th>
124
- <Th>
125
- <Typography variant="sigma">{ getMessage('page.discover.table.header.lastUpdate') }</Typography>
126
- </Th>
127
- <Th>
128
- <Typography variant="sigma">{ getMessage('page.discover.table.header.status') }</Typography>
129
- </Th>
130
- <Th>
131
- <VisuallyHidden>{ getMessage('page.discover.table.header.actions') }</VisuallyHidden>
132
- </Th>
133
- </Tr>
134
- </Thead>
135
- <Tbody>
136
- {result.map(entry => (<DiscoverTableRow key={`comment-${entry.id}`} config={config} item={entry} onClick={() => handleClickDisplay(entry.id)}/>))}
137
- </Tbody>
138
- </Table>
139
- <TablePagination pagination={{ pageCount: pagination?.pageCount || 1 }} />
140
- </>) : (<EmptyStateLayout content={emptyLayout[emptyContent]} />) }
141
- </ContentLayout>
142
- </>) }
143
- </Layout>
144
- </Box>;
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.message') }</Typography>
119
+ </Th>
120
+ <Th>
121
+ <Typography variant="sigma">{ getMessage('page.discover.table.header.thread') }</Typography>
122
+ </Th>
123
+ <Th>
124
+ <Typography variant="sigma">{ getMessage('page.discover.table.header.entry') }</Typography>
125
+ </Th>
126
+ <Th>
127
+ <Typography variant="sigma">{ getMessage('page.discover.table.header.lastUpdate') }</Typography>
128
+ </Th>
129
+ <Th>
130
+ <Typography variant="sigma">{ getMessage('page.discover.table.header.status') }</Typography>
131
+ </Th>
132
+ <Th>
133
+ <VisuallyHidden>{ getMessage('page.discover.table.header.actions') }</VisuallyHidden>
134
+ </Th>
135
+ </Tr>
136
+ </Thead>
137
+ <Tbody>
138
+ {result.map(entry => (<DiscoverTableRow
139
+ key={`comment-${entry.id}`}
140
+ config={config}
141
+ item={entry}
142
+ allowedActions={{ canModerate, canAccessReports, canReviewReports }}
143
+ onClick={() => handleClickDisplay(entry.id)}
144
+ />))}
145
+ </Tbody>
146
+ </Table>
147
+ <TablePagination pagination={{ pageCount: pagination?.pageCount || 1 }} />
148
+ </>) : (<EmptyStateLayout content={emptyLayout[emptyContent]} />) }
149
+ </ContentLayout>
150
+ </>) }
151
+ </Layout>
152
+ </Box>;
153
+ }
154
+ return null;
145
155
  }
146
156
 
147
157
  const mapStateToProps = makeAppView();
@@ -1,14 +1,16 @@
1
+ const permissions = require('./../../permissions');
2
+
1
3
  const pluginPermissions = {
2
4
  access: [
3
- { action: 'plugin::comments.read', subject: null },
4
- { action: 'plugin::comments.reports.read', subject: null },
5
- { action: 'plugin::comments.discovery.read', subject: null },
6
- { action: 'plugin::comments.settings.read', subject: null },
5
+ { action: permissions.render(permissions.comments.read), subject: null },
6
+ { action: permissions.render(permissions.reports.read), subject: null },
7
+ // { action: 'plugin::comments.discover.read', subject: null },
8
+ { action: permissions.render(permissions.settings.read), subject: null },
7
9
  ],
8
- discover: [{ action: 'plugin::comments.read', subject: null }],
9
- moderate: [{ action: 'plugin::comments.moderate', subject: null }],
10
- reports: [{ action: 'plugin::comments.reports.read', subject: null }],
11
- reportsReview: [{ action: 'plugin::comments.reports.action', subject: null }],
10
+ // discover: [{ action: 'plugin::comments.discover.read', subject: null }],
11
+ moderate: [{ action: permissions.render(permissions.comments.moderate), subject: null }],
12
+ reports: [{ action: permissions.render(permissions.reports.read), subject: null }],
13
+ reportsReview: [{ action: permissions.render(permissions.reports.action), subject: null }],
12
14
  };
13
15
 
14
16
  export default pluginPermissions;
@@ -76,6 +76,7 @@
76
76
  "page.details.actions.comment.approve.confirmation.success": "Comment has been approved",
77
77
  "page.details.actions.comment.reject.confirmation.success": "Comment has been rejected",
78
78
  "page.details.filters.label": "View",
79
+ "page.details.filters.removed.visibility": "Comments visibility",
79
80
  "page.details.filters.removed.show": "Show removed",
80
81
  "page.details.filters.removed.hide": "Hide removed",
81
82
  "page.coming.soon": "Wait for what is coming soon...",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strapi-plugin-comments",
3
- "version": "2.0.0-beta.5",
3
+ "version": "2.0.0-beta.6",
4
4
  "description": "Strapi - Comments plugin",
5
5
  "strapi": {
6
6
  "name": "comments",
package/permissions.js ADDED
@@ -0,0 +1,19 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ render: function(uid) {
5
+ return `plugin::comments.${uid}`;
6
+ },
7
+ comments: {
8
+ read: 'read',
9
+ moderate: 'moderate',
10
+ },
11
+ reports: {
12
+ read: 'reports.read',
13
+ action: 'reports.action',
14
+ },
15
+ settings: {
16
+ read: 'settings.read',
17
+ change: 'settings.change',
18
+ },
19
+ };
@@ -1,5 +1,41 @@
1
1
  'use strict';
2
2
 
3
- module.exports = ({ strapi }) => {
4
- // bootstrap phase
3
+ const permissions = require('./../permissions');
4
+
5
+ module.exports = async ({ strapi }) => {
6
+ // Check if the plugin users-permissions is installed because the navigation needs it
7
+ if (Object.keys(strapi.plugins).indexOf("users-permissions") === -1) {
8
+ throw new Error(
9
+ "In order to make the comments plugin work the users-permissions plugin is required",
10
+ );
11
+ }
12
+ // Add permissions
13
+ const actions = [
14
+ {
15
+ section: "plugins",
16
+ displayName: "Comments: Read",
17
+ uid: permissions.comments.read,
18
+ pluginName: "comments",
19
+ },
20
+ {
21
+ section: "plugins",
22
+ displayName: "Comments: Moderate",
23
+ uid: permissions.comments.moderate,
24
+ pluginName: "comments",
25
+ },
26
+ {
27
+ section: "plugins",
28
+ displayName: "Reports: Read",
29
+ uid: permissions.reports.read,
30
+ pluginName: "comments",
31
+ },
32
+ {
33
+ section: "plugins",
34
+ displayName: "Reports: Moderate",
35
+ uid: permissions.reports.action,
36
+ pluginName: "comments",
37
+ },
38
+ ];
39
+
40
+ await strapi.admin.services.permission.actionProvider.registerMany(actions);
5
41
  };
@@ -2,10 +2,10 @@
2
2
 
3
3
  module.exports = {
4
4
  default: {
5
+ moderatorRoles: [],
5
6
  approvalFlow: [],
6
7
  entryLabel: {
7
8
  '*': ['Title', 'title', 'Name', 'name', 'Subject', 'subject']
8
9
  },
9
- },
10
- validator() {},
10
+ }
11
11
  };
@@ -88,12 +88,13 @@ const { APPROVAL_STATUS, REGEX } = require('./../utils/constants')
88
88
  .map(_ => filterOurResolvedReports(this.getCommonService().sanitizeCommentEntity(_)))
89
89
  .map(_ => this.getCommonService().mergeRelatedEntityTo(_, relatedEntities));
90
90
 
91
+ const pageCount = Math.floor(total / pageSize);
91
92
  return {
92
93
  result,
93
94
  pagination: {
94
95
  page: page,
95
96
  pageSize: pageSize,
96
- pageCount: Math.floor(total / pageSize) + 1,
97
+ pageCount: total % pageSize === 0 ? pageCount : pageCount + 1,
97
98
  total,
98
99
  },
99
100
  };
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const config = require('./../config');
3
+ const config = require('../config');
4
4
  const { getPluginService } = require('./../utils/functions');
5
5
  const { isNil, isEmpty } = require('lodash');
6
6
  const PluginError = require('./../utils/error');
package/strapi-server.js CHANGED
@@ -1,9 +1,42 @@
1
1
  'use strict';
2
2
 
3
+ const { isArray, isEmpty, isString } = require('lodash');
4
+
3
5
  const server = require('./server');
4
6
  const contentTypes = require('./content-types');
7
+ const { REGEX } = require('./server/utils/constants');
5
8
 
6
9
  module.exports = () => ({
7
10
  ...server,
8
11
  contentTypes,
12
+ config: {
13
+ ...server.config,
14
+ default: ({ env }) => ({
15
+ ...server.config.default,
16
+ }),
17
+ validator: (config) => {
18
+ // Check moderatorRoles values
19
+ const moderatorRolesValid = config.moderatorRoles
20
+ .filter(_ => !isString(_)).length === 0;
21
+ if (!moderatorRolesValid) {
22
+ throw new Error(`'moderatorRoles' must roles keys`);
23
+ }
24
+
25
+ // Check approvalFlow values
26
+ const approvalFlowValid = config.approvalFlow
27
+ .filter(_ => !REGEX.uid.test(_)).length === 0;
28
+ if (!approvalFlowValid) {
29
+ throw new Error(`'approvalFlow' must contain Content Types identifiers in the format like 'api::<collection name>.<content type name>'`);
30
+ }
31
+
32
+ // Check entryLabel keys and values
33
+ const entryLabelKeysValid = Object.keys(config.entryLabel)
34
+ .filter(_ => _ !== '*')
35
+ .filter(_ => !(REGEX.uid.test(_) && isArray(config.entryLabel[_]) && !isEmpty(config.entryLabel[_]))).length === 0;
36
+
37
+ if (!entryLabelKeysValid) {
38
+ throw new Error(`'entryLabel' must contain records in the format like 'api::<collection name>.<content type name>': ['Field1', 'field_2']`);
39
+ }
40
+ },
41
+ },
9
42
  });