strapi-plugin-comments 2.0.0-beta.6 → 2.0.0
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/.circleci/config.yml +1 -1
- package/README.md +474 -119
- package/__mocks__/initSetup.js +53 -0
- package/admin/src/components/ActionButton/styles.js +10 -0
- package/admin/src/components/DiscussionThread/index.js +3 -7
- package/admin/src/components/DiscussionThreadItem/index.js +25 -74
- package/admin/src/components/DiscussionThreadItem/styles.js +4 -79
- package/admin/src/components/DiscussionThreadItemActions/index.js +82 -40
- package/admin/src/components/DiscussionThreadItemActions/styles.js +1 -12
- package/admin/src/components/DiscussionThreadItemApprovalFlowActions/index.js +2 -3
- package/admin/src/components/DiscussionThreadItemFooter/index.js +47 -0
- package/admin/src/components/DiscussionThreadItemFooter/styles.js +13 -0
- package/admin/src/components/DiscussionThreadItemReviewAction/index.js +131 -0
- package/admin/src/components/IconButton/styles.js +14 -0
- package/admin/src/components/ReportsReviewModal/index.js +1 -1
- package/admin/src/components/ReportsReviewTable/index.js +12 -6
- package/admin/src/components/StatusBadge/index.js +17 -0
- package/admin/src/components/StatusBadge/styles.js +17 -0
- package/admin/src/pages/Details/components/DetailsEntity/index.js +40 -24
- package/admin/src/pages/Details/components/DetailsFilters/index.js +8 -10
- package/admin/src/pages/Details/index.js +9 -15
- package/admin/src/pages/Discover/components/DiscoverTableRow/index.js +36 -66
- package/admin/src/pages/Discover/index.js +4 -1
- package/admin/src/translations/en.json +14 -15
- package/admin/src/utils/index.js +2 -0
- package/admin/src/utils/renderInitials.js +5 -0
- package/admin/src/utils/resolveCommentStatus.js +4 -4
- package/content-types/comment.js +0 -4
- package/package.json +8 -4
- package/public/assets/logo.png +0 -0
- package/public/assets/preview.png +0 -0
- package/server/config/index.js +5 -0
- package/server/controllers/client.js +11 -21
- package/server/controllers/utils/__tests__/functions.test.js +2 -2
- package/server/controllers/utils/parsers.js +19 -0
- package/server/graphql/index.js +26 -0
- package/server/graphql/mutations/createAbuseReport.js +25 -0
- package/server/graphql/mutations/createComment.js +25 -0
- package/server/graphql/mutations/index.js +21 -0
- package/server/graphql/mutations/removeComment.js +25 -0
- package/server/graphql/mutations/updateComment.js +25 -0
- package/server/graphql/queries/findAllFlat.js +29 -0
- package/server/graphql/queries/findAllInHierarchy.js +31 -0
- package/server/graphql/queries/index.js +19 -0
- package/server/graphql/resolvers-config.js +15 -0
- package/server/graphql/types/comment-author.js +9 -0
- package/server/graphql/types/comment-nested.js +16 -0
- package/server/graphql/types/comment.js +15 -0
- package/server/graphql/types/create-comment-author.js +9 -0
- package/server/graphql/types/create-comment.js +9 -0
- package/server/graphql/types/create-report.js +9 -0
- package/server/graphql/types/id.js +24 -0
- package/server/graphql/types/identify-comment-author.js +6 -0
- package/server/graphql/types/index.js +16 -0
- package/server/graphql/types/remove-comment.js +8 -0
- package/server/graphql/types/report-reason.js +10 -0
- package/server/graphql/types/report.js +11 -0
- package/server/graphql/types/update-comment.js +9 -0
- package/server/register.js +3 -1
- package/server/services/__tests__/common.test.js +165 -0
- package/server/services/admin.js +18 -15
- package/server/services/client.js +73 -51
- package/server/services/common.js +42 -14
- package/server/services/graphql.js +190 -0
- package/server/services/index.js +2 -0
- package/server/services/utils/__tests__/functions.test.js +9 -73
- package/server/services/utils/functions.js +30 -27
- package/server/utils/__tests__/functions.test.js +17 -0
- package/admin/src/components/DiscussionThreadItemMeta/index.js +0 -31
- package/admin/src/components/DiscussionThreadItemWarnings/index.js +0 -147
- package/admin/src/components/DiscussionThreadItemWarnings/styles.js +0 -7
|
@@ -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
|
+
};
|
package/server/services/index.js
CHANGED
|
@@ -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
|
};
|
|
@@ -1,86 +1,22 @@
|
|
|
1
|
-
const PluginError = require('
|
|
2
|
-
const {
|
|
1
|
+
const PluginError = require('../../../utils/error');
|
|
2
|
+
const { resolveUserContextError, getModelUid } = require('../functions');
|
|
3
3
|
|
|
4
4
|
beforeEach(() => {
|
|
5
5
|
Object.defineProperty(global, 'strapi', {
|
|
6
|
-
value:
|
|
7
|
-
plugins: {
|
|
8
|
-
comments: {
|
|
9
|
-
package: require('../../../package.json'),
|
|
10
|
-
services: {
|
|
11
|
-
comments: require('../../comments'),
|
|
12
|
-
},
|
|
13
|
-
models: {
|
|
14
|
-
comment: require('../../../models/comment.settings.json'),
|
|
15
|
-
report: require('../../../models/report.settings.json'),
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
},
|
|
19
|
-
config: {
|
|
20
|
-
plugins: {
|
|
21
|
-
comments: {
|
|
22
|
-
enableUsers: false,
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
},
|
|
6
|
+
value: require('../../../../__mocks__/initSetup')(),
|
|
27
7
|
writable: true,
|
|
28
8
|
});
|
|
29
9
|
})
|
|
30
10
|
|
|
31
|
-
describe('Test
|
|
32
|
-
|
|
33
|
-
describe('Bad Words filtering', () => {
|
|
34
|
-
const text = 'Lorem ipsum dolor sit fuck amet';
|
|
35
|
-
|
|
36
|
-
test('Should find bad words usage and throw error PluginError', () => {
|
|
37
|
-
expect.assertions(6);
|
|
38
|
-
try {
|
|
39
|
-
checkBadWords(text);
|
|
40
|
-
} catch (e) {
|
|
41
|
-
expect(e).toBeInstanceOf(PluginError);
|
|
42
|
-
expect(e).toHaveProperty('status', 400);
|
|
43
|
-
expect(e).toHaveProperty('name', 'Strapi:Plugin:Comments');
|
|
44
|
-
expect(e).toHaveProperty('message', 'Bad language used! Please polite your comment...');
|
|
45
|
-
expect(e).toHaveProperty('payload');
|
|
46
|
-
expect(e.payload).toEqual(expect.objectContaining({
|
|
47
|
-
content: {
|
|
48
|
-
original: text,
|
|
49
|
-
filtered: text.replace('fuck', '****'),
|
|
50
|
-
}
|
|
51
|
-
}));
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test('Should skip bad words filtering because of configuration change', () => {
|
|
56
|
-
global.strapi.config.plugins.comments.badWords = false;
|
|
57
|
-
|
|
58
|
-
expect(checkBadWords(text)).toEqual(text);
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
describe('Extracting metadata', () => {
|
|
63
|
-
test('Should extract plugin metadata properly', () => {
|
|
64
|
-
const { model, service, plugin, pluginName } = extractMeta(strapi.plugins);
|
|
65
|
-
|
|
66
|
-
expect(model).toHaveProperty('info.name', 'comment');
|
|
67
|
-
expect(service).toHaveProperty('findAll');
|
|
68
|
-
expect(Object.keys(plugin)).toEqual(expect.arrayContaining(['package', 'services', 'models']));
|
|
69
|
-
expect(pluginName).toBe('comments');
|
|
70
|
-
});
|
|
71
|
-
});
|
|
11
|
+
describe('Test service functions utils', () => {
|
|
72
12
|
|
|
73
|
-
describe('
|
|
74
|
-
test('
|
|
75
|
-
|
|
76
|
-
expect(isValidUserContext(undefined)).toEqual(true);
|
|
77
|
-
expect(isValidUserContext({ id: 1 })).toEqual(true);
|
|
13
|
+
describe('Getting models uid\'s ', () => {
|
|
14
|
+
test('Should get comments uid', () => {
|
|
15
|
+
expect(getModelUid('comment')).toBe('plugins::comments.comment');
|
|
78
16
|
});
|
|
79
17
|
|
|
80
|
-
test('
|
|
81
|
-
|
|
82
|
-
expect(isValidUserContext(undefined)).toEqual(false);
|
|
83
|
-
expect(isValidUserContext({ id: 1 })).toEqual(true);
|
|
18
|
+
test('Should get report uid', () => {
|
|
19
|
+
expect(getModelUid('comment-report')).toBe('plugins::comments.comment-report');
|
|
84
20
|
});
|
|
85
21
|
});
|
|
86
22
|
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
const BadWordsFilter = require('bad-words');
|
|
2
1
|
const PluginError = require('./../../utils/error');
|
|
3
2
|
const { REGEX } = require('./../../utils/constants')
|
|
4
|
-
const { first,
|
|
3
|
+
const { first, isObject, isEmpty } = require('lodash');
|
|
5
4
|
|
|
6
5
|
const buildNestedStructure = (
|
|
7
6
|
entities,
|
|
8
7
|
id = null,
|
|
9
|
-
field = '
|
|
8
|
+
field = 'threadOf',
|
|
10
9
|
dropBlockedThreads = false,
|
|
11
10
|
blockNestedThreads = false,
|
|
12
11
|
) =>
|
|
@@ -33,7 +32,7 @@ const buildNestedStructure = (
|
|
|
33
32
|
|
|
34
33
|
module.exports = {
|
|
35
34
|
isEqualEntity: (existing, data, user) => {
|
|
36
|
-
const {
|
|
35
|
+
const { author: existingAuthor } = existing;
|
|
37
36
|
const { author } = data;
|
|
38
37
|
|
|
39
38
|
// Disallow approval status change by Client
|
|
@@ -42,12 +41,12 @@ module.exports = {
|
|
|
42
41
|
}
|
|
43
42
|
|
|
44
43
|
// Make sure that author is exact the same
|
|
45
|
-
if (
|
|
46
|
-
const existingUserId =
|
|
47
|
-
const receivedUserId = user?.id || author;
|
|
48
|
-
return existingUserId === receivedUserId;
|
|
44
|
+
if (user) {
|
|
45
|
+
const existingUserId = existingAuthor?.id || existingAuthor;
|
|
46
|
+
const receivedUserId = user?.id || author?.id;
|
|
47
|
+
return receivedUserId && (existingUserId === receivedUserId);
|
|
49
48
|
}
|
|
50
|
-
return
|
|
49
|
+
return existingAuthor.id === author?.id;
|
|
51
50
|
},
|
|
52
51
|
|
|
53
52
|
getRelatedGroups: related => related.split(REGEX.relatedUid).filter(s => s && s.length > 0),
|
|
@@ -63,21 +62,6 @@ module.exports = {
|
|
|
63
62
|
reports: (item.reports || []).filter(report => !report.resolved),
|
|
64
63
|
} : item),
|
|
65
64
|
|
|
66
|
-
checkBadWords: content => {
|
|
67
|
-
const config = get(strapi.config, 'plugins.comments.badWords', true);
|
|
68
|
-
if (config) {
|
|
69
|
-
const filter = new BadWordsFilter(isObject(config) ? config : undefined);
|
|
70
|
-
if (content && filter.isProfane(content)) {
|
|
71
|
-
throw new PluginError(400, 'Bad language used! Please polite your comment...', {
|
|
72
|
-
content: {
|
|
73
|
-
original: content,
|
|
74
|
-
filtered: content && filter.clean(content),
|
|
75
|
-
},
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
return content;
|
|
80
|
-
},
|
|
81
65
|
convertContentTypeNameToSlug: str => {
|
|
82
66
|
const plainConversion = str.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`);
|
|
83
67
|
return first(plainConversion) === '-' ? plainConversion.slice(1, plainConversion.length) : plainConversion;
|
|
@@ -85,9 +69,28 @@ module.exports = {
|
|
|
85
69
|
|
|
86
70
|
buildNestedStructure,
|
|
87
71
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
72
|
+
buildAuthorModel: (item) => {
|
|
73
|
+
const { authorUser, authorId, authorName, authorEmail, authorAvatar, ...rest } = item;
|
|
74
|
+
let author = {};
|
|
75
|
+
if (authorUser) {
|
|
76
|
+
author = {
|
|
77
|
+
id: authorUser.id,
|
|
78
|
+
name: authorUser.username,
|
|
79
|
+
email: authorUser.email,
|
|
80
|
+
avatar: authorUser.avatar,
|
|
81
|
+
};
|
|
82
|
+
} else if(authorId) {
|
|
83
|
+
author = {
|
|
84
|
+
id: authorId,
|
|
85
|
+
name: authorName,
|
|
86
|
+
email: authorEmail,
|
|
87
|
+
avatar: authorAvatar,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
...rest,
|
|
92
|
+
author: isEmpty(author) ? undefined : author,
|
|
93
|
+
};
|
|
91
94
|
},
|
|
92
95
|
|
|
93
96
|
resolveUserContextError: user => {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const { getPluginService } = require('../functions');
|
|
2
|
+
|
|
3
|
+
beforeEach(() => {
|
|
4
|
+
Object.defineProperty(global, 'strapi', {
|
|
5
|
+
value: require('../../../__mocks__/initSetup')(),
|
|
6
|
+
writable: true,
|
|
7
|
+
});
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
describe('Test plugin functions utils', () => {
|
|
11
|
+
|
|
12
|
+
describe('Get plugin service', () => {
|
|
13
|
+
test('Should get common service', () => {
|
|
14
|
+
expect(getPluginService('common')).toHaveProperty('findAllFlat');
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
* Entity Details
|
|
4
|
-
*
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import React from 'react';
|
|
8
|
-
import PropTypes from 'prop-types';
|
|
9
|
-
import { useIntl } from 'react-intl';
|
|
10
|
-
import { Box } from '@strapi/design-system/Box';
|
|
11
|
-
import { Flex } from '@strapi/design-system/Flex';
|
|
12
|
-
import { Typography } from '@strapi/design-system/Typography';
|
|
13
|
-
|
|
14
|
-
const DiscussionThreadItemMeta = ({ authorName, authorEmail, createdAt, updatedAt }) => {
|
|
15
|
-
|
|
16
|
-
const { formatDate } = useIntl();
|
|
17
|
-
|
|
18
|
-
const dateTime = formatDate(updatedAt || createdAt, { dateStyle: 'short', timeStyle: 'short' });
|
|
19
|
-
|
|
20
|
-
return (<Flex as={Box} paddingTop={2} direction="row" justifyContent="space-between">
|
|
21
|
-
<Typography variant="pi" fontWeight="bold">{ authorName }</Typography>
|
|
22
|
-
<Typography variant="pi">{ dateTime }</Typography>
|
|
23
|
-
</Flex>);
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
DiscussionThreadItemMeta.propTypes = {
|
|
27
|
-
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export default DiscussionThreadItemMeta;
|
|
31
|
-
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
* Entity Details
|
|
4
|
-
*
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import React, { useState } from 'react';
|
|
8
|
-
import PropTypes from 'prop-types';
|
|
9
|
-
import { useMutation, useQueryClient } from 'react-query';
|
|
10
|
-
import { isEmpty, orderBy } from 'lodash';
|
|
11
|
-
import { Badge } from '@strapi/design-system/Badge';
|
|
12
|
-
import { Box } from '@strapi/design-system/Box';
|
|
13
|
-
import { Button } from '@strapi/design-system/Button';
|
|
14
|
-
import { Divider } from '@strapi/design-system/Divider';
|
|
15
|
-
import { Flex } from '@strapi/design-system/Flex';
|
|
16
|
-
import { Typography } from '@strapi/design-system/Typography';
|
|
17
|
-
import { useNotification, useOverlayBlocker } from '@strapi/helper-plugin';
|
|
18
|
-
import { getMessage, handleAPIError } from '../../utils';
|
|
19
|
-
import { blockItem, blockItemThread, resolveReport } from '../../pages/utils/api';
|
|
20
|
-
import { DiscussionThreadItemWarningsWrapper } from './styles';
|
|
21
|
-
import ReportsReviewModal from '../ReportsReviewModal';
|
|
22
|
-
import ReportsReviewTable from '../ReportsReviewTable';
|
|
23
|
-
import pluginId from '../../pluginId';
|
|
24
|
-
import { LockIcon, ReviewIcon } from '../icons';
|
|
25
|
-
|
|
26
|
-
const DiscussionThreadItemWarnings = (item) => {
|
|
27
|
-
const { id, reports, gotThread, allowedActions: { canModerate, canAccessReports, canReviewReports }} = item;
|
|
28
|
-
|
|
29
|
-
const [reportsReviewVisible, setReportsReviewVisible] = useState(false);
|
|
30
|
-
const [blockButtonsDisabled, setBlockButtonsDisabled] = useState(false);
|
|
31
|
-
|
|
32
|
-
const toggleNotification = useNotification();
|
|
33
|
-
const queryClient = useQueryClient();
|
|
34
|
-
const { lockApp, unlockApp } = useOverlayBlocker();
|
|
35
|
-
|
|
36
|
-
const onSuccess = (message, stateAction = () => {}, indalidate = true) => async () => {
|
|
37
|
-
if (indalidate) {
|
|
38
|
-
await queryClient.invalidateQueries('get-details-data');
|
|
39
|
-
}
|
|
40
|
-
toggleNotification({
|
|
41
|
-
type: 'success',
|
|
42
|
-
message: `${pluginId}.${message}`,
|
|
43
|
-
});
|
|
44
|
-
stateAction(false);
|
|
45
|
-
unlockApp();
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const onError = err => { handleAPIError(err, toggleNotification); };
|
|
49
|
-
|
|
50
|
-
const resolveReportMutation = useMutation(resolveReport, {
|
|
51
|
-
onSuccess: onSuccess('page.details.panel.discussion.warnings.reports.dialog.confirmation.success', () => {}, false),
|
|
52
|
-
onError,
|
|
53
|
-
refetchActive: false,
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
const blockItemMutation = useMutation(blockItem, {
|
|
57
|
-
onSuccess: onSuccess(
|
|
58
|
-
'page.details.actions.comment.block.confirmation.success',
|
|
59
|
-
setReportsReviewVisible
|
|
60
|
-
),
|
|
61
|
-
onError,
|
|
62
|
-
refetchActive: false,
|
|
63
|
-
});
|
|
64
|
-
const blockItemThreadMutation = useMutation(blockItemThread, {
|
|
65
|
-
onSuccess: onSuccess(
|
|
66
|
-
'page.details.actions.thread.block.confirmation.success',
|
|
67
|
-
setReportsReviewVisible
|
|
68
|
-
),
|
|
69
|
-
onError,
|
|
70
|
-
refetchActive: false,
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
const handleReportsReviewClick = () => setReportsReviewVisible(true);
|
|
74
|
-
const handleBlockItemClick = async () => {
|
|
75
|
-
lockApp();
|
|
76
|
-
blockItemMutation.mutate(id);
|
|
77
|
-
};
|
|
78
|
-
const handleBlockItemThreadClick = async () => {
|
|
79
|
-
lockApp();
|
|
80
|
-
blockItemThreadMutation.mutate(id);
|
|
81
|
-
};
|
|
82
|
-
const handleReportsReviewClose = async () => {
|
|
83
|
-
await queryClient.invalidateQueries('get-details-data')
|
|
84
|
-
setReportsReviewVisible(false);
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
const handleBlockButtonsStateChange = disabled => setBlockButtonsDisabled(disabled);
|
|
88
|
-
|
|
89
|
-
const isLoading = blockItemMutation.isLoading || blockItemThreadMutation.isLoading || resolveReportMutation.isLoading;
|
|
90
|
-
const openReports = reports?.filter(_ => !_.resolved);
|
|
91
|
-
|
|
92
|
-
if (isEmpty(reports) || isEmpty(openReports)) {
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return (<>
|
|
97
|
-
{ canAccessReports && (<DiscussionThreadItemWarningsWrapper as={Flex} direction="row" padding={2} background="danger100" borderColor="danger200" border={1} hasRadius>
|
|
98
|
-
<Box as={Flex} paddingRight={2} direction="row" grow={1}>
|
|
99
|
-
<Badge textColor="neutral0" backgroundColor="warning600">{ openReports.length }</Badge>
|
|
100
|
-
<Box paddingLeft={2}>
|
|
101
|
-
<Typography textColor="warning600" fontWeight="bold">{getMessage(`page.details.panel.discussion.warnings.reports.description`, 'abuse reports')}</Typography>
|
|
102
|
-
</Box>
|
|
103
|
-
</Box>
|
|
104
|
-
<Button onClick={handleReportsReviewClick} variant="ghost" endIcon={<ReviewIcon />}>
|
|
105
|
-
{ getMessage(`page.details.actions.comment.report.review`, 'review') }
|
|
106
|
-
</Button>
|
|
107
|
-
</DiscussionThreadItemWarningsWrapper>) }
|
|
108
|
-
{ (canAccessReports || canReviewReports) && (<ReportsReviewModal
|
|
109
|
-
isVisible={reportsReviewVisible}
|
|
110
|
-
isActionAsync={isLoading}
|
|
111
|
-
onClose={handleReportsReviewClose}
|
|
112
|
-
startActions={canModerate && (<>
|
|
113
|
-
<Button onClick={handleBlockItemClick} variant="danger-light" startIcon={<LockIcon />} disabled={blockButtonsDisabled}>
|
|
114
|
-
{ getMessage(`page.details.actions.comment.block`, 'Block comment') }
|
|
115
|
-
</Button>
|
|
116
|
-
{ gotThread && (<Button onClick={handleBlockItemThreadClick} variant="danger" startIcon={<LockIcon />} disabled={blockButtonsDisabled}>
|
|
117
|
-
{ getMessage(`page.details.actions.thread.block`, 'Block thread') }
|
|
118
|
-
</Button>) }
|
|
119
|
-
</>)}
|
|
120
|
-
item={item}>
|
|
121
|
-
<Typography variant="sigma" textColor="neutral600" id="reports-list">
|
|
122
|
-
{ getMessage('page.details.panel.discussion.warnings.reports.list', 'List') }
|
|
123
|
-
</Typography>
|
|
124
|
-
<Box paddingTop={2} paddingBottom={6}>
|
|
125
|
-
<Divider />
|
|
126
|
-
</Box>
|
|
127
|
-
<ReportsReviewTable
|
|
128
|
-
commentId={id}
|
|
129
|
-
items={orderBy(reports, ['resolved', 'createdAt'], ['DESC', 'DESC'])}
|
|
130
|
-
mutation={resolveReportMutation}
|
|
131
|
-
allowedActions={{ canAccessReports, canReviewReports }}
|
|
132
|
-
onBlockButtonsStateChange={handleBlockButtonsStateChange}
|
|
133
|
-
/>
|
|
134
|
-
</ReportsReviewModal>) }
|
|
135
|
-
</>);
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
DiscussionThreadItemWarnings.propTypes = {
|
|
139
|
-
allowedActions: PropTypes.shape({
|
|
140
|
-
canModerate: PropTypes.bool,
|
|
141
|
-
canAccessReports: PropTypes.bool,
|
|
142
|
-
canReviewReports: PropTypes.bool,
|
|
143
|
-
}),
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
export default DiscussionThreadItemWarnings;
|
|
147
|
-
|