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
@@ -0,0 +1,190 @@
1
+ 'use strict';
2
+
3
+ const { has, propEq, isNil, isDate, isObject } = require('lodash/fp');
4
+ const { inputObjectType } = require('nexus');
5
+
6
+ const virtualScalarAttributes = ['id'];
7
+
8
+ module.exports = ({ strapi }) => {
9
+ const { service: getService } = strapi.plugin('graphql');
10
+
11
+ const rootLevelOperators = () => {
12
+ const { operators } = strapi.plugin('graphql').service('builders').filters;
13
+
14
+ return [operators.and, operators.or, operators.not];
15
+ };
16
+
17
+ const buildContentTypeFilters = contentType => {
18
+ const utils = strapi.plugin('graphql').service('utils');
19
+ const extension = strapi.plugin('graphql').service('extension');
20
+
21
+ const { getFiltersInputTypeName, getScalarFilterInputTypeName } = utils.naming;
22
+ const { isStrapiScalar, isRelation } = utils.attributes;
23
+
24
+ const { attributes } = contentType;
25
+
26
+ const filtersTypeName = getFiltersInputTypeName(contentType);
27
+
28
+ return inputObjectType({
29
+ name: filtersTypeName,
30
+
31
+ definition(t) {
32
+ const validAttributes = Object.entries(attributes).filter(([attributeName]) =>
33
+ extension
34
+ .shadowCRUD(contentType.uid)
35
+ .field(attributeName)
36
+ .hasFiltersEnabeld()
37
+ );
38
+
39
+ const isIDFilterEnabled = extension
40
+ .shadowCRUD(contentType.uid)
41
+ .field('id')
42
+ .hasFiltersEnabeld();
43
+ // Add an ID filter to the collection types
44
+ if (contentType.kind === 'collectionType' && isIDFilterEnabled) {
45
+ t.field('id', { type: getScalarFilterInputTypeName('ID') });
46
+ }
47
+
48
+ // Add every defined attribute
49
+ for (const [attributeName, attribute] of validAttributes) {
50
+ // Handle scalars
51
+ if (isStrapiScalar(attribute)) {
52
+ addScalarAttribute(t, attributeName, attribute);
53
+ }
54
+
55
+ // Handle relations
56
+ else if (isRelation(attribute)) {
57
+ addRelationalAttribute(t, attributeName, attribute);
58
+ }
59
+ }
60
+
61
+ // Conditional clauses
62
+ for (const operator of rootLevelOperators()) {
63
+ operator.add(t, filtersTypeName);
64
+ }
65
+ },
66
+ });
67
+ };
68
+
69
+ const addScalarAttribute = (builder, attributeName, attribute) => {
70
+ const { naming, mappers } = strapi.plugin('graphql').service('utils');
71
+
72
+ const gqlType = mappers.strapiScalarToGraphQLScalar(attribute.type);
73
+
74
+ builder.field(attributeName, { type: naming.getScalarFilterInputTypeName(gqlType) });
75
+ };
76
+
77
+ const addRelationalAttribute = (builder, attributeName, attribute) => {
78
+ const utils = strapi.plugin('graphql').service('utils');
79
+ const extension = strapi.plugin('graphql').service('extension');
80
+ const { getFiltersInputTypeName } = utils.naming;
81
+ const { isMorphRelation } = utils.attributes;
82
+
83
+ const model = strapi.getModel(attribute.target);
84
+
85
+ // If there is no model corresponding to the attribute configuration
86
+ // or if the attribute is a polymorphic relation, then ignore it
87
+ if (!model || isMorphRelation(attribute)) return;
88
+
89
+ // If the target model is disabled, then ignore it too
90
+ if (extension.shadowCRUD(model.uid).isDisabled()) return;
91
+
92
+ builder.field(attributeName, { type: getFiltersInputTypeName(model) });
93
+ };
94
+
95
+ const recursivelyReplaceScalarOperators = data => {
96
+ const { operators } = getService('builders').filters;
97
+
98
+ if (Array.isArray(data)) {
99
+ return data.map(recursivelyReplaceScalarOperators);
100
+ }
101
+
102
+ // Note: We need to make an exception for date since GraphQL
103
+ // automatically cast date strings to date instances in args
104
+ if (isDate(data) || !isObject(data)) {
105
+ return data;
106
+ }
107
+
108
+ const result = {};
109
+
110
+ for (const [key, value] of Object.entries(data)) {
111
+ const isOperator = !!operators[key];
112
+
113
+ const newKey = isOperator ? operators[key].strapiOperator : key;
114
+
115
+ result[newKey] = recursivelyReplaceScalarOperators(value);
116
+ }
117
+
118
+ return result;
119
+ };
120
+
121
+ const graphQLFiltersToStrapiQuery = (filters, contentType = {}) => {
122
+ const { isStrapiScalar, isMedia, isRelation } = getService('utils').attributes;
123
+ const { operators } = getService('builders').filters;
124
+
125
+ const ROOT_LEVEL_OPERATORS = [operators.and, operators.or, operators.not];
126
+
127
+ // Handle unwanted scenario where there is no filters defined
128
+ if (isNil(filters)) {
129
+ return {};
130
+ }
131
+
132
+ // If filters is a collection, then apply the transformation to every item of the list
133
+ if (Array.isArray(filters)) {
134
+ return filters.map(filtersItem =>
135
+ graphQLFiltersToStrapiQuery(filtersItem, contentType)
136
+ );
137
+ }
138
+
139
+ const resultMap = {};
140
+ const { attributes } = contentType;
141
+
142
+ const isAttribute = attributeName => {
143
+ return virtualScalarAttributes.includes(attributeName) || has(attributeName, attributes);
144
+ };
145
+
146
+ for (const [key, value] of Object.entries(filters)) {
147
+ // If the key is an attribute, update the value
148
+ if (isAttribute(key)) {
149
+ const attribute = attributes[key];
150
+
151
+ // If it's a scalar attribute
152
+ if (virtualScalarAttributes.includes(key) || isStrapiScalar(attribute)) {
153
+ // Replace (recursively) every GraphQL scalar operator with the associated Strapi operator
154
+ resultMap[key] = recursivelyReplaceScalarOperators(value);
155
+ }
156
+
157
+ // If it's a deep filter on a relation
158
+ else if (isRelation(attribute) || isMedia(attribute)) {
159
+ // Fetch the model from the relation
160
+ const relModel = strapi.getModel(attribute.target);
161
+
162
+ // Recursively apply the mapping to the value using the fetched model,
163
+ // and update the value within `resultMap`
164
+ resultMap[key] = graphQLFiltersToStrapiQuery(value, relModel);
165
+ }
166
+ }
167
+
168
+ // Handle the case where the key is not an attribute (operator, ...)
169
+ else {
170
+ const rootLevelOperator = ROOT_LEVEL_OPERATORS.find(propEq('fieldName', key));
171
+
172
+ // If it's a root level operator (AND, NOT, OR, ...)
173
+ if (rootLevelOperator) {
174
+ const { strapiOperator } = rootLevelOperator;
175
+
176
+ // Transform the current value recursively and add it to the resultMap
177
+ // object using the strapiOperator equivalent of the GraphQL key
178
+ resultMap[strapiOperator] = graphQLFiltersToStrapiQuery(value, contentType);
179
+ }
180
+ }
181
+ }
182
+
183
+ return resultMap;
184
+ };
185
+
186
+ return {
187
+ buildContentTypeFilters,
188
+ graphQLFiltersToStrapiQuery,
189
+ };
190
+ };
@@ -3,9 +3,11 @@
3
3
  const adminServices = require('./admin');
4
4
  const clientServices = require('./client');
5
5
  const commonServices = require('./common');
6
+ const graphQLService = require('./graphql');
6
7
 
7
8
  module.exports = {
8
9
  admin: adminServices,
9
10
  client: clientServices,
10
11
  common: commonServices,
12
+ gql: graphQLService,
11
13
  };
@@ -6,7 +6,7 @@ const { first, get, isObject, isNil } = require('lodash');
6
6
  const buildNestedStructure = (
7
7
  entities,
8
8
  id = null,
9
- field = 'parent',
9
+ field = 'threadOf',
10
10
  dropBlockedThreads = false,
11
11
  blockNestedThreads = false,
12
12
  ) =>
@@ -33,28 +33,21 @@ const buildNestedStructure = (
33
33
 
34
34
  module.exports = {
35
35
  isEqualEntity: (existing, data, user) => {
36
- const { authorUser, authorId } = existing;
37
- console.log('data', data);
38
- console.log(existing);
36
+ const { authorUser, author: existingAuthor } = existing;
39
37
  const { author } = data;
38
+
39
+ // Disallow approval status change by Client
40
+ if (data.approvalStatus && (existing.approvalStatus !== data.approvalStatus)) {
41
+ return false;
42
+ }
43
+
44
+ // Make sure that author is exact the same
40
45
  if (authorUser) {
41
46
  const existingUserId = authorUser?.id || authorUser;
42
47
  const receivedUserId = user?.id || author;
43
48
  return existingUserId === receivedUserId;
44
49
  }
45
- return authorId === author?.id;
46
- },
47
-
48
- extractMeta: plugins => {
49
- const { comments: plugin } = plugins;
50
- const { comments: service } = plugin.services;
51
- const { comment: model} = plugin.models;
52
- return {
53
- model,
54
- service,
55
- plugin,
56
- pluginName: plugin.package.strapi.name.toLowerCase()
57
- };
50
+ return existingAuthor.id === author?.id;
58
51
  },
59
52
 
60
53
  getRelatedGroups: related => related.split(REGEX.relatedUid).filter(s => s && s.length > 0),
@@ -97,6 +90,20 @@ module.exports = {
97
90
  return builtInContextEnabled ? !isNil(user.id) : true;
98
91
  },
99
92
 
93
+ buildAuthorModel: (item) => {
94
+ const { authorUser, authorId, authorName, authorEmail, authorAvatar, ...rest } = item;
95
+ const author = authorUser || {
96
+ id: authorId,
97
+ name: authorName,
98
+ email: authorEmail,
99
+ avatar: authorAvatar,
100
+ };
101
+ return {
102
+ ...rest,
103
+ author,
104
+ };
105
+ },
106
+
100
107
  resolveUserContextError: user => {
101
108
  if (user) {
102
109
  throw new PluginError(401, 'Not authenticated');
@@ -5,9 +5,9 @@ const APPROVAL_STATUS = {
5
5
  };
6
6
 
7
7
  const REGEX = {
8
- uid: new RegExp(/^(?<type>[a-z]+)\:{2}(?<api>[a-z]+)\.{1}(?<contentType>[a-z]+)$/gmi),
9
- relatedUid: new RegExp(/^(?<uid>[a-z]+\:{2}[a-z]+\.[a-z]+)\:{1}(?<id>[0-9]+)$/gmi),
10
- email: new RegExp(/\S+@\S+\.\S+/),
8
+ uid: /^(?<type>[a-z0-9-]+)\:{2}(?<api>[a-z0-9-]+)\.{1}(?<contentType>[a-z0-9-]+)$/i,
9
+ relatedUid: /^(?<uid>[a-z0-9-]+\:{2}[a-z0-9-]+\.[a-z0-9-]+)\:{1}(?<id>[a-z0-9-]+)$/i,
10
+ email: /\S+@\S+\.\S+/,
11
11
  };
12
12
 
13
13
  module.exports = {
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
  });
@@ -1,53 +0,0 @@
1
- /**
2
- *
3
- * Entity Details
4
- *
5
- */
6
-
7
- import React from 'react';
8
- import PropTypes from 'prop-types';
9
- import { isNil } 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 EntityDetails = ({ data = {}, schema = {} }) => {
18
- const { attributes = {} } = schema;
19
- const keys = Object.keys(attributes);
20
-
21
- if (keys.length === 0) {
22
- return null;
23
- }
24
-
25
- const itemKeys = keys.filter(_ => (attributes[_].type === 'string') && !isNil(data[_]));
26
-
27
- return (<Box padding={4}>
28
- <Typography variant="sigma" textColor="neutral600" id="entity-details">
29
- { getMessage('page.details.panel.entity', 'Details') }
30
- </Typography>
31
- <Box paddingTop={2} paddingBottom={6}>
32
- <Divider />
33
- </Box>
34
- <Stack size={itemKeys.length}>
35
- { itemKeys.map(_ =>
36
- (<Flex justifyContent="space-between">
37
- <Typography fontWeight="bold">
38
- { _ }
39
- </Typography>
40
- <Typography>{ data[_] }</Typography>
41
- </Flex>))
42
- }
43
- </Stack>
44
- </Box>);
45
- };
46
-
47
- EntityDetails.propTypes = {
48
- data: PropTypes.object.isRequired,
49
- schema: PropTypes.object.isRequired,
50
- };
51
-
52
- export default EntityDetails;
53
-