strapi-plugin-comments 2.0.4 → 2.0.8

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
@@ -84,7 +84,7 @@ In our minimum support we're following [official Node.js releases timelines](htt
84
84
 
85
85
  **Supported Strapi versions**:
86
86
 
87
- - Strapi v4.0.7 (recently tested)
87
+ - Strapi v4.1.0 (recently tested)
88
88
  - Strapi v4.x
89
89
 
90
90
  > This plugin is designed for **Strapi v4** and is not working with v3.x. To get version for **Strapi v3** install version [v1.x](https://github.com/VirtusLab-Open-Source/strapi-plugin-comments/tree/strapi-v3).
@@ -109,6 +109,8 @@ To setup amend default plugin configuration we recommend to put following snippe
109
109
  module.exports = ({ env }) => ({
110
110
  //...
111
111
  comments: {
112
+ enabled: true,
113
+ config: {
112
114
  badWords: false,
113
115
  moderatorRoles: ["Authenticated"],
114
116
  approvalFlow: ['api::page.page'],
@@ -122,6 +124,7 @@ To setup amend default plugin configuration we recommend to put following snippe
122
124
  gql: {
123
125
  // ...
124
126
  },
127
+ },
125
128
  },
126
129
  //...
127
130
  });
@@ -144,8 +147,8 @@ All you need to do is to install and enable `@strapi/plugin-graphql` for you ins
144
147
  ```json
145
148
  {
146
149
  // ...
147
- gql: {
148
- auth: true, // Default: false
150
+ "gql": {
151
+ "auth": true, // Default: false
149
152
  },
150
153
  // ...
151
154
  }
@@ -226,10 +229,10 @@ Return a hierarchical tree structure of comments for specified instance of Conte
226
229
  [
227
230
  {
228
231
  // -- Comment Model fields ---,
229
- children: [
232
+ "children": [
230
233
  {
231
234
  // -- Comment Model fields ---,
232
- children: [
235
+ "children": [
233
236
  // ...
234
237
  ]
235
238
  },
@@ -255,7 +258,8 @@ Return a flat structure of comments for specified instance of Content Type like
255
258
  **Example response body**
256
259
 
257
260
  ```json
258
- [
261
+ {
262
+ "data": [
259
263
  {
260
264
  // -- Comment Model fields ---
261
265
  },
@@ -263,7 +267,13 @@ Return a flat structure of comments for specified instance of Content Type like
263
267
  // -- Comment Model fields ---
264
268
  },
265
269
  // ...
266
- ]
270
+ ],
271
+ "meta": {
272
+ "pagination": {
273
+ // payload based on Strapi REST Pagination specification
274
+ }
275
+ }
276
+ }
267
277
  ```
268
278
 
269
279
  **Possible response codes**
@@ -1,14 +1,32 @@
1
- const { get, set } = require('lodash');
1
+ const { get, set, isEmpty } = require('lodash');
2
2
 
3
- module.exports = (config = {}, toStore = false) => {
4
- const mock = {
5
- db: toStore ? {
6
- plugin: {
7
- comments: {
8
- config: { ...config },
9
- },
3
+ module.exports = (config = {}, toStore = false, database = {}) => {
4
+ const dbConfig = toStore ? {
5
+ plugin: {
6
+ comments: {
7
+ config: { ...config },
10
8
  },
11
- }: {},
9
+ },
10
+ }: {};
11
+
12
+ let mock = {
13
+ db: {
14
+ query: (uid) => {
15
+ const [handler, rest] = uid.split('::');
16
+ const [collection] = rest.split('.');
17
+ const values = get(mock.db, `${handler}.${collection}.records`, []);
18
+ return {
19
+ findOne: async () => new Promise(resolve => resolve(values[0])),
20
+ findMany: async () => new Promise(resolve => resolve(values)),
21
+ findWithCount: async () => new Promise(resolve => resolve([values, values.length])),
22
+ count: async () => new Promise(resolve => resolve(values.length)),
23
+ create: async (value) => new Promise(resolve => resolve(value)),
24
+ update: async (value) => new Promise(resolve => resolve(value)),
25
+ delete: async (value) => new Promise(resolve => resolve(value)),
26
+ };
27
+ },
28
+ ...dbConfig,
29
+ },
12
30
  getRef: function() {
13
31
  return this;
14
32
  },
@@ -78,5 +96,13 @@ module.exports = (config = {}, toStore = false) => {
78
96
  }
79
97
  }
80
98
  };
99
+
100
+ if (!isEmpty(database)) {
101
+ Object.keys(database).forEach((uid) => {
102
+ const [handler, collection] = uid.split('::');
103
+ set(mock.db, `${handler}.${collection}.records`, database[uid]);
104
+ });
105
+ }
106
+
81
107
  return mock;
82
108
  };
@@ -0,0 +1,18 @@
1
+ /**
2
+ *
3
+ * Form Switch
4
+ *
5
+ */
6
+
7
+ import React from 'react';
8
+ import { Stack } from '@strapi/design-system/Stack';
9
+ import { Typography } from '@strapi/design-system/Typography';
10
+ import { StyledSwitch } from './styles';
11
+
12
+ const FormSwitch = ({ label, hint, ...props }) => (<Stack size={1}>
13
+ { label && (<Typography variant="pi" as="label" fontWeight="bold" textColor="neutral800">{ label }</Typography>) }
14
+ <StyledSwitch label={label} { ...props } />
15
+ { hint && (<Typography variant="pi" as="label" textColor="neutral600">{ hint }</Typography>) }
16
+ </Stack>);
17
+
18
+ export default FormSwitch;
@@ -0,0 +1,8 @@
1
+ import styled from 'styled-components';
2
+ import { Switch } from '@strapi/design-system/Switch';
3
+
4
+ export const StyledSwitch = styled(Switch)`
5
+ &>div>span {
6
+ font-size: 12px;
7
+ }
8
+ `;
@@ -1,7 +1,7 @@
1
1
  import React, { useRef, useMemo, useState } from 'react';
2
2
  import { useQuery } from 'react-query';
3
3
  import { Formik } from 'formik';
4
- import { capitalize, first, orderBy, isEmpty, isEqual } from 'lodash';
4
+ import { capitalize, first, orderBy, isEmpty, isEqual, isNil } from 'lodash';
5
5
  import {
6
6
  CheckPermissions,
7
7
  LoadingIndicatorPage,
@@ -12,22 +12,20 @@ import {
12
12
  useFocusWhenNavigate,
13
13
  useOverlayBlocker,
14
14
  } from '@strapi/helper-plugin';
15
+ import { Accordion, AccordionToggle, AccordionContent, AccordionGroup } from '@strapi/design-system/Accordion';
15
16
  import { Main } from '@strapi/design-system/Main';
16
17
  import { ContentLayout, HeaderLayout } from '@strapi/design-system/Layout';
17
18
  import { Button } from '@strapi/design-system/Button';
18
19
  import { Box } from '@strapi/design-system/Box';
19
20
  import { Stack } from '@strapi/design-system/Stack';
21
+ import { Switch } from '@strapi/design-system/Switch';
20
22
  import { Typography } from '@strapi/design-system/Typography';
21
23
  import { Grid, GridItem } from '@strapi/design-system/Grid';
22
24
  import { ToggleInput } from '@strapi/design-system/ToggleInput';
23
25
  import { Select, Option } from '@strapi/design-system/Select';
24
26
  import { useNotifyAT } from '@strapi/design-system/LiveRegions';
25
- import {
26
- Card,
27
- CardBody,
28
- CardContent,
29
- } from '@strapi/design-system/Card';
30
- import { Check, Refresh, Play } from '@strapi/icons';
27
+ import { Tooltip } from '@strapi/design-system/Tooltip';
28
+ import { Check, Refresh, Play, Information } from '@strapi/icons';
31
29
 
32
30
  import pluginPermissions from '../../permissions';
33
31
  import useConfig from '../../hooks/useConfig';
@@ -35,6 +33,7 @@ import { fetchAllContentTypes, fetchRoles } from './utils/api';
35
33
  import { getMessage, parseRegExp } from '../../utils';
36
34
  import ConfirmationDialog from '../../components/ConfirmationDialog';
37
35
  import { RestartAlert } from './components/RestartAlert/styles';
36
+ import FormSwitch from '../../components/FormSwitch';
38
37
 
39
38
 
40
39
  const Settings = () => {
@@ -58,6 +57,7 @@ const Settings = () => {
58
57
 
59
58
  const [restoreConfigmationVisible, setRestoreConfigmationVisible] = useState(false);
60
59
  const [restartRequired, setRestartRequired] = useState(false);
60
+ const [contentTypeExpanded, setContentTypeExpanded] = useState(undefined);
61
61
 
62
62
  const { fetch, restartMutation, submitMutation, restoreMutation } = useConfig(toggleNotification);
63
63
  const { data: configData, isLoading: isConfigLoading, err: configErr } = fetch;
@@ -109,7 +109,7 @@ const Settings = () => {
109
109
  .filter(_ => _) || [];
110
110
  const entryLabel = configData?.entryLabel || {};
111
111
  const approvalFlow = configData?.approvalFlow || [];
112
- const badWords = configData?.badWords || undefined;
112
+ const badWords = isNil(configData?.badWords) ? true : configData?.badWords;
113
113
  const isGQLPluginEnabled = configData?.isGQLPluginEnabled;
114
114
  const gqlAuthEnabled = configData?.gql?.auth || undefined;
115
115
  const moderatorRoles = configData?.moderatorRoles
@@ -166,6 +166,8 @@ const Settings = () => {
166
166
  };
167
167
  const handleRestartDiscard = () => setRestartRequired(false);
168
168
 
169
+ const handleSetContentTypeExpanded = key => setContentTypeExpanded(key === contentTypeExpanded ? undefined : key);
170
+
169
171
  const boxDefaultProps = {
170
172
  background: "neutral0",
171
173
  hasRadius: true,
@@ -201,7 +203,7 @@ const Settings = () => {
201
203
  }
202
204
  />
203
205
  <ContentLayout>
204
- <Stack size={6}>
206
+ <Stack size={4}>
205
207
  { restartRequired && (
206
208
  <RestartAlert
207
209
  closeLabel={getMessage('page.settings.actions.restart.alert.cancel')}
@@ -234,6 +236,72 @@ const Settings = () => {
234
236
  (<Option key={uid} value={uid}>{displayName}</Option>))}
235
237
  </Select>
236
238
  </GridItem>
239
+ { !isEmpty(values.enabledCollections) && (
240
+ <GridItem col={12}>
241
+ <AccordionGroup
242
+ label={getMessage('page.settings.form.contentTypesSettings.label')}
243
+ labelAction={<Tooltip description={getMessage('page.settings.form.contentTypesSettings.tooltip')}>
244
+ <Information aria-hidden={true} />
245
+ </Tooltip>}>
246
+ { orderBy(values.enabledCollections).map(uid => {
247
+ const { schema: { displayName, attributes = {} } } = allCollections.find(_ => _.uid === uid);
248
+ const stringAttributes = Object.keys(attributes).filter(_ => attributes[_].type === 'string');
249
+ const key = `collectionSettings-${uid}`;
250
+ return (<Accordion
251
+ expanded={contentTypeExpanded === key}
252
+ toggle={() => handleSetContentTypeExpanded(key)}
253
+ key={key}
254
+ id={key}
255
+ size="S">
256
+ <AccordionToggle title={displayName} togglePosition="left" />
257
+ <AccordionContent>
258
+ <Box padding={6}>
259
+ <Stack size={4}>
260
+ <FormSwitch
261
+ name={`collectionSettings-${uid}-approvalFlow`}
262
+ label={getMessage('page.settings.form.approvalFlow.label')}
263
+ hint={getMessage({
264
+ id: 'page.settings.form.approvalFlow.hint',
265
+ props: { name: displayName },
266
+ })}
267
+ selected={values.approvalFlow.includes(uid)}
268
+ onChange={() => setFieldValue('approvalFlow', changeApprovalFlowFor(uid, values.approvalFlow, !values.approvalFlow.includes(uid)), [])}
269
+ onLabel={getMessage('compontents.toogle.enabled')}
270
+ offLabel={getMessage('compontents.toogle.disabled')}
271
+ disabled={restartRequired}
272
+ visibleLabels />
273
+ { !isEmpty(stringAttributes) && (<Select
274
+ name={`collectionSettings-${uid}-entryLabel`}
275
+ label={getMessage('page.settings.form.entryLabel.label')}
276
+ placeholder={getMessage('page.settings.form.entryLabel.placeholder')}
277
+ hint={getMessage('page.settings.form.entryLabel.hint')}
278
+ onClear={() => setFieldValue('entryLabel', changeEntryLabelFor(uid, values.entryLabel))}
279
+ value={values.entryLabel[uid] || []}
280
+ onChange={(value) => setFieldValue('entryLabel', changeEntryLabelFor(uid, values.entryLabel, value))}
281
+ multi
282
+ withTags
283
+ disabled={restartRequired}
284
+ >
285
+ { stringAttributes.map(key =>
286
+ (<Option key={`collectionSettings-${uid}-entryLabel-${key}`} value={key}>{ capitalize(key.split('_').join(' ')) }</Option>))}
287
+ </Select>) }
288
+ </Stack>
289
+ </Box>
290
+ </AccordionContent>
291
+ </Accordion>);
292
+ })}
293
+ </AccordionGroup>
294
+ </GridItem>)}
295
+ </Grid>
296
+ </Stack>
297
+ </Box>
298
+
299
+ <Box {...boxDefaultProps}>
300
+ <Stack size={4}>
301
+ <Typography variant="delta" as="h2">
302
+ {getMessage('page.settings.section.additional')}
303
+ </Typography>
304
+ <Grid gap={4}>
237
305
  <GridItem col={6} xs={12}>
238
306
  <Select
239
307
  name="moderatorRoles"
@@ -278,64 +346,12 @@ const Settings = () => {
278
346
  </Grid>
279
347
  </Stack>
280
348
  </Box>
281
- { !isEmpty(values.enabledCollections) && (<Box {...boxDefaultProps}>
282
- <Stack size={4}>
283
- <Typography variant="delta" as="h2">
284
- {getMessage('page.settings.section.collections')}
285
- </Typography>
286
- <Grid gap={4}>
287
- { orderBy(values.enabledCollections).map(uid => {
288
- const { schema: { displayName, attributes = {} } } = allCollections.find(_ => _.uid === uid);
289
- const stringAttributes = Object.keys(attributes).filter(_ => attributes[_].type === 'string');
290
- return (<GridItem key={`collectionSettings-${uid}`} col={6} s={12} xs={12}>
291
- <Card background="primary100" borderColor="primary200">
292
- <CardBody>
293
- <CardContent style={{ width: '100%' }}>
294
- <Stack size={4}>
295
- <Typography variant="epsilon" fontWeight="semibold" as="h3">{ displayName }</Typography>
296
- <ToggleInput
297
- name={`collectionSettings-${uid}-approvalFlow`}
298
- label={getMessage('page.settings.form.approvalFlow.label')}
299
- hind={getMessage({
300
- id: 'page.settings.form.approvalFlow.hint',
301
- params: { name: displayName },
302
- })}
303
- checked={values.approvalFlow.includes(uid)}
304
- onChange={({ target: { checked } }) => setFieldValue('approvalFlow', changeApprovalFlowFor(uid, values.approvalFlow, checked), [])}
305
- onLabel={getMessage('compontents.toogle.enabled')}
306
- offLabel={getMessage('compontents.toogle.disabled')}
307
- disabled={restartRequired}
308
- />
309
- { !isEmpty(stringAttributes) && (<Select
310
- name={`collectionSettings-${uid}-entryLabel`}
311
- label={getMessage('page.settings.form.entryLabel.label')}
312
- placeholder={getMessage('page.settings.form.entryLabel.placeholder')}
313
- hint={getMessage('page.settings.form.entryLabel.hint')}
314
- onClear={() => setFieldValue('entryLabel', changeEntryLabelFor(uid, values.entryLabel))}
315
- value={values.entryLabel[uid] || []}
316
- onChange={(value) => setFieldValue('entryLabel', changeEntryLabelFor(uid, values.entryLabel, value))}
317
- multi
318
- withTags
319
- disabled={restartRequired}
320
- >
321
- { stringAttributes.map(key =>
322
- (<Option key={`collectionSettings-${uid}-entryLabel-${key}`} value={key}>{ capitalize(key.split('_').join(' ')) }</Option>))}
323
- </Select>) }
324
- </Stack>
325
- </CardContent>
326
- </CardBody>
327
- </Card>
328
- </GridItem>);
329
- })}
330
- </Grid>
331
- </Stack>
332
- </Box>)}
333
349
 
334
350
  <CheckPermissions permissions={pluginPermissions.settingsChange}>
335
351
  <Box {...boxDefaultProps}>
336
352
  <Stack size={4}>
337
353
  <Stack size={2}>
338
- <Typography variant="delta" textColor="danger700" as="h2">
354
+ <Typography variant="delta" as="h2">
339
355
  {getMessage('page.settings.section.restore')}
340
356
  </Typography>
341
357
  <Typography variant="pi"as="h4">
@@ -85,15 +85,17 @@
85
85
  "page.settings.header.title": "Comments",
86
86
  "page.settings.header.description": "Configure your comments moderation capabilities",
87
87
  "page.settings.actions.submit": "Save",
88
- "page.settings.actions.restore": "Restore",
88
+ "page.settings.actions.restore": "Restore default settings",
89
89
  "page.settings.actions.restart": "Restart Strapi",
90
90
  "page.settings.section.general": "General configuration",
91
- "page.settings.section.collections": "Collection(s) configuration",
92
- "page.settings.section.restore": "Restore default configuration",
91
+ "page.settings.section.additional": "Additional configuration",
92
+ "page.settings.section.restore": "Restore default settings",
93
93
  "page.settings.section.restore.subtitle": "Discarding all of applied settings and getting back to plugin default ones. Use reasonable.",
94
- "page.settings.form.enabledCollections.label": "Enable Comments for Collection(s)",
94
+ "page.settings.form.enabledCollections.label": "Enable comments only for",
95
95
  "page.settings.form.enabledCollections.placeholder": "Select one or more collection",
96
- "page.settings.form.enabledCollections.hint": "If none of collection is selected, feature are enabled for any of exsiting",
96
+ "page.settings.form.enabledCollections.hint": "If none is selected, all the content types are enabled",
97
+ "page.settings.form.contentTypesSettings.label": "Content types",
98
+ "page.settings.form.contentTypesSettings.tooltip": "Custom configuration per content type",
97
99
  "page.settings.form.moderatorRoles.label": "Send significant notifications to",
98
100
  "page.settings.form.moderatorRoles.placeholder": "Select one or more roles",
99
101
  "page.settings.form.moderatorRoles.hint": "Roles which are going to be notified by the plugin about significant actions to perform",
@@ -101,8 +103,8 @@
101
103
  "page.settings.form.badWords.hint": "If enabled, every post / update of comment is going to be checked against bad wording",
102
104
  "page.settings.form.gqlAuth.label": "GraphQL queries authorization",
103
105
  "page.settings.form.gqlAuth.hint": "If enabled, GraphQL API queries & mutations can be triggered only by Authenticated Strapi users. Otherwise API remains open.",
104
- "page.settings.form.approvalFlow.label": "Enable approval flow",
105
- "page.settings.form.approvalFlow.hint": "Comments for \"{name}\" are going to be taken through manual approval flow",
106
+ "page.settings.form.approvalFlow.label": "Approval flow",
107
+ "page.settings.form.approvalFlow.hint": "Comments associated with content type \"{name}\" are going to be taken through manual approval flow",
106
108
  "page.settings.form.entryLabel.label": "Title fields",
107
109
  "page.settings.form.entryLabel.placeholder": "Select at least one or leave empty to apply defaults",
108
110
  "page.settings.form.entryLabel.hint": "If left empty title rendering is going to take following ordered fields: \"Title\", \"Subject\" & \"Name\"",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strapi-plugin-comments",
3
- "version": "2.0.4",
3
+ "version": "2.0.8",
4
4
  "description": "Strapi - Comments plugin",
5
5
  "strapi": {
6
6
  "name": "comments",
@@ -18,8 +18,8 @@
18
18
  "url": "https://github.com/VirtusLab-Open-Source/strapi-plugin-comments"
19
19
  },
20
20
  "dependencies": {
21
- "@strapi/utils": "^4.0.7",
22
- "@strapi/plugin-graphql": "^4.0.7",
21
+ "@strapi/utils": "^4.1.0",
22
+ "@strapi/plugin-graphql": "^4.1.0",
23
23
  "bad-words": "^3.0.4"
24
24
  },
25
25
  "devDependencies": {
@@ -13,9 +13,15 @@ module.exports = {
13
13
  async findAllFlat(ctx) {
14
14
  const { params = {}, query, sort, pagination } = ctx;
15
15
  const { relation } = parseParams(params);
16
+
17
+ const { sort: querySort, pagination: queryPagination, ...filterQuery } = query || {};
18
+
19
+
20
+ console.log(filterQuery);
21
+
16
22
  try {
17
23
  return this.getService('common')
18
- .findAllFlat(flatInput(relation, query, sort, pagination));
24
+ .findAllFlat(flatInput(relation, filterQuery, sort || querySort, pagination || queryPagination));
19
25
  } catch (e) {
20
26
  throwError(ctx, e);
21
27
  }
@@ -24,10 +30,13 @@ module.exports = {
24
30
  async findAllInHierarchy(ctx) {
25
31
  const { params = {}, query, sort } = ctx;
26
32
  const { relation } = parseParams(params);
33
+
34
+ const { sort: querySort, ...filterQuery } = query || {};
35
+
27
36
  try {
28
37
  return await this.getService('common')
29
38
  .findAllInHierarchy({
30
- ...flatInput(relation, query, sort),
39
+ ...flatInput(relation, filterQuery, sort || querySort),
31
40
  dropBlockedThreads: true,
32
41
  });
33
42
  } catch (e) {
@@ -0,0 +1,94 @@
1
+ const { flatInput } = require('../parsers');
2
+
3
+ jest.mock
4
+
5
+ describe('Test Comments controller parsers utils', () => {
6
+
7
+ describe('Flat Input properties', () => {
8
+ const relation = 'api::test.relation';
9
+ const filters = {
10
+ content: {
11
+ '$eq': 'Test'
12
+ }
13
+ };
14
+ const sort = {
15
+ createdAt: 'desc'
16
+ };
17
+ const pagination = {
18
+ page: 3,
19
+ pageSize: 5,
20
+ };
21
+
22
+ test('Should contain default property \'populate\'', () => {
23
+ expect(flatInput()).toHaveProperty(['populate', 'threadOf', 'populate', 'authorUser'], true);
24
+ });
25
+
26
+ test('Should assign relation to \'query\'', () => {
27
+ expect(flatInput(relation)).toHaveProperty(['query', 'related'], relation);
28
+ });
29
+
30
+ test('Should assign filters to \'query\'', () => {
31
+ const result = flatInput(relation, filters);
32
+ expect(result).toHaveProperty(['query', 'content', '$eq'], filters.content['$eq']);
33
+ });
34
+
35
+ test('Should assign not allow to overwrite \'removed\' in $or operator', () => {
36
+ const overwrittenOrOperator = {
37
+ $or: [{ removed: true }, { removed: false }, { removed: null }]
38
+ };
39
+ const result = flatInput(relation, overwrittenOrOperator);
40
+ expect(result).toHaveProperty(['query', '$or', 0, 'removed', '$null'], true);
41
+ expect(result).toHaveProperty(['query', '$or', 1, 'removed'], false);
42
+ });
43
+
44
+ test('Should assign sort', () => {
45
+ const result = flatInput(relation, filters, sort);
46
+ expect(result).toHaveProperty(['sort', 'createdAt'], sort.createdAt);
47
+ });
48
+
49
+ test('Should assign pagination', () => {
50
+ const result = flatInput(relation, filters, sort, pagination);
51
+ expect(result).toHaveProperty(['pagination', 'page'], pagination.page);
52
+ expect(result).toHaveProperty(['pagination', 'pageSize'], pagination.pageSize);
53
+ });
54
+
55
+ test('Should build complex output', () => {
56
+ const day = 24 * 60 * 60 * 1000;
57
+ const now = Date.now().toString();
58
+ const oneDayBefore = new Date(Date.now().valueOf() - day).toString();
59
+ const complexFilters = {
60
+ '$or': [{
61
+ createdAt: {
62
+ '$lte': oneDayBefore
63
+ }
64
+ }, {
65
+ createdAt: {
66
+ '$eq': now
67
+ }
68
+ }],
69
+ content: {
70
+ '$contains': 'Test'
71
+ }
72
+ };
73
+
74
+ const complexSort = [{
75
+ createdAt: 'desc'
76
+ }, {
77
+ title: 'asc'
78
+ }];
79
+
80
+ const result = flatInput(relation, complexFilters, complexSort, pagination);
81
+
82
+ expect(result).toHaveProperty(['query', 'related'], relation);
83
+ expect(result).toHaveProperty(['query', '$or', 0, 'createdAt', '$lte'], oneDayBefore);
84
+ expect(result).toHaveProperty(['query', '$or', 1, 'createdAt', '$eq'], now);
85
+ expect(result).toHaveProperty(['query', '$or', 2, 'removed', '$null'], true);
86
+ expect(result).toHaveProperty(['query', '$or', 3, 'removed'], false);
87
+ expect(result).toHaveProperty(['query', 'content', '$contains'], complexFilters.content['$contains']);
88
+ expect(result).toHaveProperty(['sort', 0, 'createdAt'], complexSort[0].createdAt);
89
+ expect(result).toHaveProperty(['sort', 1, 'title'], complexSort[1].title);
90
+ expect(result).toHaveProperty(['pagination', 'page'], pagination.page);
91
+ expect(result).toHaveProperty(['pagination', 'pageSize'], pagination.pageSize);
92
+ });
93
+ });
94
+ });
@@ -2,20 +2,22 @@
2
2
 
3
3
  module.exports = {
4
4
  flatInput(relation, query, sort, pagination) {
5
- return {
6
- query: {
7
- ...query,
8
- $or: [{ removed: { $null: true } }, { removed: false }],
9
- related: relation,
5
+ const filters = query?.filters || query;
6
+ const orOperator = (filters?.$or || []).filter(_ => !Object.keys(_).includes('removed'));
7
+ return {
8
+ query: {
9
+ ...filters,
10
+ $or: [...orOperator, { removed: { $null: true } }, { removed: false }],
11
+ related: relation,
12
+ },
13
+ populate: {
14
+ threadOf: {
15
+ populate: { authorUser: true },
10
16
  },
11
- populate: {
12
- threadOf: {
13
- populate: { authorUser: true },
14
- },
15
- },
16
- pagination,
17
- sort,
18
- };
17
+ },
18
+ pagination,
19
+ sort,
20
+ };
19
21
  },
20
22
 
21
23
  };
@@ -10,7 +10,7 @@ module.exports = ({ strapi, nexus }) => {
10
10
  const { args } = getService('internals');
11
11
 
12
12
  return {
13
- type: nonNull(list('CommentSingle')),
13
+ type: 'ResponseFindAll',
14
14
  args: {
15
15
  relation: nonNull(stringArg()),
16
16
  filters: getPluginService('gql').buildContentTypeFilters(contentType),
@@ -11,6 +11,9 @@ const typesFactories = [
11
11
  require('./report'),
12
12
  require('./report-reason'),
13
13
  require('./create-report'),
14
+ require('./response-pagination'),
15
+ require('./response-meta'),
16
+ require('./response-find-all'),
14
17
  ];
15
18
 
16
19
  module.exports = context => typesFactories.map(factory => factory(context));
@@ -0,0 +1,7 @@
1
+ module.exports = ({ nexus }) => nexus.objectType({
2
+ name: "ResponseFindAll",
3
+ definition(t) {
4
+ t.list.field("data", { type: 'CommentSingle' })
5
+ t.field("meta", { type: 'ResponseMeta' })
6
+ }
7
+ })
@@ -0,0 +1,6 @@
1
+ module.exports = ({ nexus }) => nexus.objectType({
2
+ name: "ResponseMeta",
3
+ definition(t) {
4
+ t.field("pagination", { type: 'ResponsePagination' })
5
+ }
6
+ })
@@ -0,0 +1,11 @@
1
+ module.exports = ({ nexus }) => nexus.objectType({
2
+ name: "ResponsePagination",
3
+ definition(t) {
4
+ t.int('page')
5
+ t.int('pageSize')
6
+ t.int('pageCount')
7
+ t.int('total')
8
+ t.int('start')
9
+ t.int('limit')
10
+ }
11
+ })
@@ -5,9 +5,9 @@ const { getPluginService } = require('../../utils/functions');
5
5
  jest.mock
6
6
 
7
7
 
8
- const setup = function(config) {
8
+ const setup = function(config, toStore, database) {
9
9
  Object.defineProperty(global, 'strapi', {
10
- value: require('../../../__mocks__/initSetup')(config),
10
+ value: require('../../../__mocks__/initSetup')(config, toStore, database),
11
11
  writable: true,
12
12
  });
13
13
  }
@@ -242,5 +242,158 @@ describe('Test Comments service functions utils', () => {
242
242
  }
243
243
  });
244
244
  });
245
+
246
+ describe('Merge related entity', () => {
247
+ beforeEach(() => setup({ enabledCollections: [uid] }));
248
+ const collection = 'api::collection.test';
249
+ const related = `${collection}:1`;
250
+ const comment = {
251
+ id: 1,
252
+ content: 'ABC',
253
+ threadOf: null,
254
+ related,
255
+ authorId: 1,
256
+ authorName: 'Joe Doe',
257
+ authorEmail: 'joe@example.com'
258
+ };
259
+ const relatedEntity = { id: 1, title: 'Test', uid: collection};
260
+
261
+
262
+ test('Should merge related entity', async () => {
263
+ const result = await getPluginService('common').mergeRelatedEntityTo(comment, [relatedEntity]);
264
+ expect(result).toHaveProperty('id', 1);
265
+ expect(result).not.toHaveProperty('related', related);
266
+ expect(result).toHaveProperty(['related', 'title'], relatedEntity.title);
267
+ });
268
+ });
269
+
270
+ describe('Client API', () => {
271
+ const collection = 'api::collection.test';
272
+ const related = `${collection}:1`;
273
+ const db = [{
274
+ id: 1,
275
+ content: 'ABC',
276
+ threadOf: null,
277
+ related,
278
+ authorId: 1,
279
+ authorName: 'Joe Doe',
280
+ authorEmail: 'joe@example.com'
281
+ }, {
282
+ id: 2,
283
+ content: 'DEF',
284
+ threadOf: 1,
285
+ related,
286
+ authorId: 1,
287
+ authorName: 'Joe Doe',
288
+ authorEmail: 'joe@example.com',
289
+ blockedThread: true,
290
+ }, {
291
+ id: 3,
292
+ content: 'GHJ',
293
+ threadOf: null,
294
+ related,
295
+ authorId: 1,
296
+ authorName: 'Joe Doe',
297
+ authorEmail: 'joe@example.com'
298
+ }, {
299
+ id: 4,
300
+ content: 'IKL',
301
+ threadOf: 2,
302
+ related,
303
+ authorId: 1,
304
+ authorName: 'Joe Doe',
305
+ authorEmail: 'joe@example.com',
306
+ }];
307
+ const relatedEntity = { id: 1, title: 'Test', uid: collection};
308
+
309
+ beforeEach(() => setup({ enabledCollections: [collection] }, true, {
310
+ 'plugins::comments': db,
311
+ 'api::collection': [relatedEntity, { id: 2, title: 'Test 2', uid: collection }],
312
+ }));
313
+
314
+ describe('findAllFlat', () => {
315
+ test('Should return proper structure', async () => {
316
+ const result = await getPluginService('common').findAllFlat({ query: { related }}, relatedEntity);
317
+ expect(result).toHaveProperty('data');
318
+ expect(result).not.toHaveProperty('meta');
319
+ expect(result.data.length).toBe(4);
320
+ expect(result).toHaveProperty(['data', 0, 'content'], db[0].content);
321
+ expect(result).toHaveProperty(['data', 3, 'content'], db[3].content);
322
+ });
323
+
324
+ test('Should return structure with pagination', async () => {
325
+ const result = await getPluginService('common').findAllFlat({ query: { related }, pagination: { page: 1, pageSize: 5 }}, relatedEntity);
326
+ expect(result).toHaveProperty('data');
327
+ expect(result).toHaveProperty('meta');
328
+ expect(result.data.length).toBe(4);
329
+ expect(result).toHaveProperty(['data', 0, 'content'], db[0].content);
330
+ expect(result).toHaveProperty(['data', 0, 'author']);
331
+ expect(result).toHaveProperty(['data', 0, 'author', 'id'], db[0].authorId);
332
+ expect(result).toHaveProperty(['data', 0, 'author', 'name'], db[0].authorName);
333
+ expect(result).toHaveProperty(['data', 0, 'author', 'email'], db[0].authorEmail);
334
+ expect(result).toHaveProperty(['data', 3, 'content'], db[3].content);
335
+ expect(result).toHaveProperty(['meta', 'pagination', 'page'], 1);
336
+ expect(result).toHaveProperty(['meta', 'pagination', 'pageSize'], 5);
337
+ });
338
+ });
339
+
340
+ describe('findAllInHierarchy', () => {
341
+ test('Should return nested structure starting for root', async () => {
342
+ const result = await getPluginService('common').findAllInHierarchy({ query: { related }}, relatedEntity);
343
+ expect(result).not.toHaveProperty('data');
344
+ expect(result).not.toHaveProperty('meta');
345
+ expect(result.length).toBe(2);
346
+ expect(result).toHaveProperty([0, 'content'], db[0].content);
347
+ expect(result).toHaveProperty([0, 'children']);
348
+ expect(result[0].children.length).toBe(1);
349
+ expect(result).toHaveProperty([0, 'children', 0, 'content'], 'DEF');
350
+ expect(result).toHaveProperty([0, 'children', 0, 'children']);
351
+ expect(result[0].children[0].children.length).toBe(1);
352
+ expect(result).toHaveProperty([0, 'children', 0, 'children', 0, 'content'], 'IKL');
353
+ expect(result).toHaveProperty([1, 'content'], db[2].content);
354
+ });
355
+
356
+ test('Should return nested structure starting for comment id: 1', async () => {
357
+ const result = await getPluginService('common').findAllInHierarchy({ query: { related }, startingFromId: 1}, relatedEntity);
358
+ expect(result).not.toHaveProperty('data');
359
+ expect(result).not.toHaveProperty('meta');
360
+ expect(result.length).toBe(1);
361
+ expect(result).toHaveProperty([0, 'content'], db[1].content);
362
+ expect(result).toHaveProperty([0, 'children']);
363
+ expect(result[0].children.length).toBe(1);
364
+ expect(result).toHaveProperty([0, 'children', 0, 'content'], 'IKL');
365
+ });
366
+
367
+ test('Should return nested structure starting for comment id: 1 without blocked comments', async () => {
368
+ const result = await getPluginService('common').findAllInHierarchy({ query: { related }, startingFromId: 1, dropBlockedThreads: true}, relatedEntity);
369
+ expect(result).not.toHaveProperty('data');
370
+ expect(result).not.toHaveProperty('meta');
371
+ expect(result.length).toBe(1);
372
+ expect(result).toHaveProperty([0, 'content'], db[1].content);
373
+ expect(result).toHaveProperty([0, 'children']);
374
+ expect(result[0].children.length).toBe(0);
375
+ });
376
+ });
377
+
378
+ describe('findOne', () => {
379
+ test('Should return proper structure', async () => {
380
+ const result = await getPluginService('common').findOne({ related });
381
+ expect(result).toHaveProperty('id', db[0].id);
382
+ expect(result).toHaveProperty('content', db[0].content);
383
+ expect(result).toHaveProperty(['author', 'id'], db[0].authorId);
384
+ expect(result).toHaveProperty(['author', 'name'], db[0].authorName);
385
+ expect(result).toHaveProperty(['author', 'email'], db[0].authorEmail);
386
+ });
387
+ });
388
+
389
+ describe('findRelatedEntitiesFor', () => {
390
+ test('Should return proper structure', async () => {
391
+ const result = await getPluginService('common').findRelatedEntitiesFor(db);
392
+ expect(result).toHaveProperty([0, 'id'], relatedEntity.id);
393
+ expect(result).toHaveProperty([0, 'title'], relatedEntity.title);
394
+ expect(result).toHaveProperty([0, 'uid'], relatedEntity.uid);
395
+ });
396
+ });
397
+ });
245
398
  });
246
399
  });
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const BadWordsFilter = require('bad-words');
4
- const { isArray, isNumber, isObject, isNil, isString, first, parseInt, set, get } = require('lodash');
4
+ const { isArray, isNumber, isObject, isNil, isString, isEmpty, first, parseInt, set, get } = require('lodash');
5
5
  const { REGEX, CONFIG_PARAMS } = require('../utils/constants');
6
6
  const PluginError = require('./../utils/error');
7
7
  const {
@@ -68,12 +68,35 @@ module.exports = ({ strapi }) => ({
68
68
  };
69
69
  }
70
70
 
71
+ let meta = {};
71
72
  if (pagination && isObject(pagination)) {
72
- const { page = 1, pageSize = 10 } = pagination;
73
+ const parsedPagination = Object.keys(pagination).reduce((prev, curr) => ({
74
+ ...prev,
75
+ [curr]: parseInt(pagination[curr]),
76
+ }), {});
77
+ const { page, pageSize = 10, limit, start = 0 } = parsedPagination
78
+ const paginationByPage = !isNil(page);
79
+
73
80
  queryExtension = {
74
81
  ...queryExtension,
75
- offset: (page - 1) * pageSize,
76
- limit: pageSize,
82
+ offset: paginationByPage ? (page - 1) * pageSize : start,
83
+ limit: paginationByPage ? pageSize : limit,
84
+ };
85
+
86
+ const metaPagination = paginationByPage ? {
87
+ pagination: {
88
+ page,
89
+ pageSize,
90
+ },
91
+ } : {
92
+ pagination: {
93
+ start,
94
+ limit,
95
+ },
96
+ };
97
+
98
+ meta = {
99
+ ...metaPagination,
77
100
  };
78
101
  }
79
102
 
@@ -89,6 +112,26 @@ module.exports = ({ strapi }) => ({
89
112
  ...queryExtension,
90
113
  });
91
114
 
115
+ if (pagination?.withCount && (pagination.withCount === 'true' || pagination.withCount === true)) {
116
+ const total = await strapi.db.query(getModelUid('comment'))
117
+ .count({
118
+ where: {
119
+ ...query,
120
+ }
121
+ });
122
+ const pageCount = Math.floor(total / meta.pagination.pageSize);
123
+ meta = {
124
+ ...meta,
125
+ pagination: {
126
+ ...meta.pagination,
127
+ pageCount: !isNil(meta.pagination.page) ?
128
+ total % meta.pagination.pageSize === 0 ? pageCount : pageCount + 1 :
129
+ undefined,
130
+ total,
131
+ },
132
+ };
133
+ }
134
+
92
135
  const entriesWithThreads = await Promise.all(entries.map(async _ => {
93
136
  const [nestedEntries, count] = await strapi.db.query(getModelUid('comment'))
94
137
  .findWithCount({
@@ -100,6 +143,8 @@ module.exports = ({ strapi }) => ({
100
143
  }));
101
144
 
102
145
  const relatedEntities = relatedEntity !== null ? [relatedEntity] : await this.findRelatedEntitiesFor([...entries]);
146
+ const hasRelatedEntitiesToMap = relatedEntities.filter(_ => _).length > 0;
147
+
103
148
  const result = entries
104
149
  .map(_ => {
105
150
  const threadedItem = entriesWithThreads.find(item => item.id === _.id);
@@ -111,10 +156,12 @@ module.exports = ({ strapi }) => ({
111
156
  });
112
157
  });
113
158
 
114
- if (relatedEntities.filter(_ => _).length > 0) {
115
- return result.map(_ => this.mergeRelatedEntityTo(_, relatedEntities));
116
- }
117
- return result;
159
+ return {
160
+ data: hasRelatedEntitiesToMap ?
161
+ result.map(_ => this.mergeRelatedEntityTo(_, relatedEntities)) :
162
+ result,
163
+ ...(isEmpty(meta) ? {} : { meta }),
164
+ };
118
165
  },
119
166
 
120
167
  // Find comments and create relations tree structure
@@ -126,7 +173,7 @@ module.exports = ({ strapi }) => ({
126
173
  dropBlockedThreads = false,
127
174
  }, relatedEntity) {
128
175
  const entities = await this.findAllFlat({ query, populate, sort }, relatedEntity);
129
- return buildNestedStructure(entities, startingFromId, 'threadOf', dropBlockedThreads, false);
176
+ return buildNestedStructure(entities?.data, startingFromId, 'threadOf', dropBlockedThreads, false);
130
177
  },
131
178
 
132
179
  // Find single comment