strapi-plugin-comments 2.0.3 → 2.0.7
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 +17 -7
- package/__mocks__/initSetup.js +35 -9
- package/admin/src/components/FormSwitch/index.js +18 -0
- package/admin/src/components/FormSwitch/styles.js +8 -0
- package/admin/src/hooks/useConfig.js +11 -5
- package/admin/src/pages/Settings/components/RestartAlert/styles.js +8 -0
- package/admin/src/pages/Settings/index.js +112 -62
- package/admin/src/pages/Settings/utils/api.js +11 -1
- package/admin/src/translations/en.json +16 -7
- package/admin/src/utils/handleAPIError.js +3 -2
- package/package.json +4 -4
- package/server/controllers/admin.js +9 -0
- package/server/controllers/client.js +11 -2
- package/server/controllers/utils/__tests__/parsers.test.js +94 -0
- package/server/controllers/utils/parsers.js +15 -13
- package/server/graphql/queries/findAllFlat.js +1 -1
- package/server/graphql/types/index.js +3 -0
- package/server/graphql/types/response-find-all.js +7 -0
- package/server/graphql/types/response-meta.js +6 -0
- package/server/graphql/types/response-pagination.js +11 -0
- package/server/routes/admin.js +8 -0
- package/server/services/__tests__/common.test.js +131 -2
- package/server/services/admin.js +5 -8
- package/server/services/client.js +2 -1
- package/server/services/common.js +57 -10
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
|
|
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**
|
package/__mocks__/initSetup.js
CHANGED
|
@@ -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
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { useQuery, useMutation, useQueryClient } from 'react-query';
|
|
2
|
-
import { fetchConfig, restoreConfig, updateConfig } from '../pages/Settings/utils/api';
|
|
2
|
+
import { fetchConfig, restartStrapi, restoreConfig, updateConfig } from '../pages/Settings/utils/api';
|
|
3
3
|
import pluginId from '../pluginId';
|
|
4
|
-
import { getMessage } from '../utils';
|
|
5
4
|
|
|
6
5
|
const useConfig = (toggleNotification) => {
|
|
7
6
|
const queryClient = useQueryClient();
|
|
@@ -18,8 +17,10 @@ const useConfig = (toggleNotification) => {
|
|
|
18
17
|
callback();
|
|
19
18
|
};
|
|
20
19
|
|
|
21
|
-
const handleSuccess = (type, callback = () => {}) => {
|
|
22
|
-
|
|
20
|
+
const handleSuccess = (type, callback = () => {}, invalidateQueries = true) => {
|
|
21
|
+
if (invalidateQueries) {
|
|
22
|
+
queryClient.invalidateQueries('get-config');
|
|
23
|
+
};
|
|
23
24
|
toggleNotification({
|
|
24
25
|
type: 'success',
|
|
25
26
|
message: `${pluginId}.page.settings.notification.${type}.success`,
|
|
@@ -37,7 +38,12 @@ const useConfig = (toggleNotification) => {
|
|
|
37
38
|
onError: () => handleError('restore'),
|
|
38
39
|
});
|
|
39
40
|
|
|
40
|
-
|
|
41
|
+
const restartMutation = useMutation(restartStrapi, {
|
|
42
|
+
onSuccess: () => handleSuccess('restart', () => {}, false),
|
|
43
|
+
onError: () => handleError('restart'),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return { fetch, restartMutation, submitMutation, restoreMutation };
|
|
41
47
|
};
|
|
42
48
|
|
|
43
49
|
export default useConfig;
|
|
@@ -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 } from 'lodash';
|
|
4
|
+
import { capitalize, first, orderBy, isEmpty, isEqual } from 'lodash';
|
|
5
5
|
import {
|
|
6
6
|
CheckPermissions,
|
|
7
7
|
LoadingIndicatorPage,
|
|
@@ -12,28 +12,28 @@ 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
|
-
|
|
27
|
-
CardBody,
|
|
28
|
-
CardContent,
|
|
29
|
-
} from '@strapi/design-system/Card';
|
|
30
|
-
import { Check, Refresh } 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';
|
|
34
32
|
import { fetchAllContentTypes, fetchRoles } from './utils/api';
|
|
35
33
|
import { getMessage, parseRegExp } from '../../utils';
|
|
36
34
|
import ConfirmationDialog from '../../components/ConfirmationDialog';
|
|
35
|
+
import { RestartAlert } from './components/RestartAlert/styles';
|
|
36
|
+
import FormSwitch from '../../components/FormSwitch';
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
const Settings = () => {
|
|
@@ -56,8 +56,10 @@ const Settings = () => {
|
|
|
56
56
|
} = useRBAC(viewPermissions);
|
|
57
57
|
|
|
58
58
|
const [restoreConfigmationVisible, setRestoreConfigmationVisible] = useState(false);
|
|
59
|
+
const [restartRequired, setRestartRequired] = useState(false);
|
|
60
|
+
const [contentTypeExpanded, setContentTypeExpanded] = useState(undefined);
|
|
59
61
|
|
|
60
|
-
const { fetch, submitMutation, restoreMutation } = useConfig(toggleNotification);
|
|
62
|
+
const { fetch, restartMutation, submitMutation, restoreMutation } = useConfig(toggleNotification);
|
|
61
63
|
const { data: configData, isLoading: isConfigLoading, err: configErr } = fetch;
|
|
62
64
|
|
|
63
65
|
const { data: allCollectionsData, isLoading: areCollectionsLoading, err: collectionsErr } = useQuery(
|
|
@@ -131,7 +133,13 @@ const Settings = () => {
|
|
|
131
133
|
const handleUpdateConfiguration = async (form) => {
|
|
132
134
|
if (canChange) {
|
|
133
135
|
lockApp();
|
|
134
|
-
|
|
136
|
+
const payload = preparePayload(form);
|
|
137
|
+
await submitMutation.mutateAsync(payload);
|
|
138
|
+
const enabledCollectionsChanged = !isEqual(payload.enabledCollections, configData?.enabledCollections);
|
|
139
|
+
const gqlAuthChanged = !isEqual(payload.gql?.auth, configData?.gql?.auth);
|
|
140
|
+
if (enabledCollectionsChanged || gqlAuthChanged) {
|
|
141
|
+
setRestartRequired(true);
|
|
142
|
+
}
|
|
135
143
|
unlockApp();
|
|
136
144
|
}
|
|
137
145
|
};
|
|
@@ -142,11 +150,24 @@ const Settings = () => {
|
|
|
142
150
|
lockApp();
|
|
143
151
|
await restoreMutation.mutateAsync();
|
|
144
152
|
unlockApp();
|
|
153
|
+
setRestartRequired(true);
|
|
145
154
|
setRestoreConfigmationVisible(false);
|
|
146
155
|
}
|
|
147
156
|
};
|
|
148
157
|
const handleRestoreCancel = () => setRestoreConfigmationVisible(false);
|
|
149
158
|
|
|
159
|
+
const handleRestart = async () => {
|
|
160
|
+
if (canChange) {
|
|
161
|
+
lockApp();
|
|
162
|
+
await restartMutation.mutateAsync();
|
|
163
|
+
setRestartRequired(false);
|
|
164
|
+
unlockApp();
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
const handleRestartDiscard = () => setRestartRequired(false);
|
|
168
|
+
|
|
169
|
+
const handleSetContentTypeExpanded = key => setContentTypeExpanded(key === contentTypeExpanded ? undefined : key);
|
|
170
|
+
|
|
150
171
|
const boxDefaultProps = {
|
|
151
172
|
background: "neutral0",
|
|
152
173
|
hasRadius: true,
|
|
@@ -175,14 +196,23 @@ const Settings = () => {
|
|
|
175
196
|
subtitle={getMessage('page.settings.header.description')}
|
|
176
197
|
primaryAction={
|
|
177
198
|
<CheckPermissions permissions={pluginPermissions.settingsChange}>
|
|
178
|
-
<Button type="submit" startIcon={<Check />} >
|
|
199
|
+
<Button type="submit" startIcon={<Check />} disabled={restartRequired} >
|
|
179
200
|
{getMessage('page.settings.actions.submit')}
|
|
180
201
|
</Button>
|
|
181
202
|
</CheckPermissions>
|
|
182
203
|
}
|
|
183
204
|
/>
|
|
184
205
|
<ContentLayout>
|
|
185
|
-
<Stack size={
|
|
206
|
+
<Stack size={4}>
|
|
207
|
+
{ restartRequired && (
|
|
208
|
+
<RestartAlert
|
|
209
|
+
closeLabel={getMessage('page.settings.actions.restart.alert.cancel')}
|
|
210
|
+
title={getMessage('page.settings.actions.restart.alert.title')}
|
|
211
|
+
action={<Box><Button onClick={handleRestart} startIcon={<Play />}>{getMessage('page.settings.actions.restart')}</Button></Box>}
|
|
212
|
+
onClose={handleRestartDiscard}>
|
|
213
|
+
{ getMessage('page.settings.actions.restart.alert.description')}
|
|
214
|
+
</RestartAlert>)}
|
|
215
|
+
|
|
186
216
|
<Box {...boxDefaultProps}>
|
|
187
217
|
<Stack size={4}>
|
|
188
218
|
<Typography variant="delta" as="h2">
|
|
@@ -198,6 +228,7 @@ const Settings = () => {
|
|
|
198
228
|
onClear={() => setFieldValue('enabledCollections', [], false)}
|
|
199
229
|
value={values.enabledCollections}
|
|
200
230
|
onChange={(value) => setFieldValue('enabledCollections', value, false)}
|
|
231
|
+
disabled={restartRequired}
|
|
201
232
|
multi
|
|
202
233
|
withTags
|
|
203
234
|
>
|
|
@@ -205,6 +236,72 @@ const Settings = () => {
|
|
|
205
236
|
(<Option key={uid} value={uid}>{displayName}</Option>))}
|
|
206
237
|
</Select>
|
|
207
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}>
|
|
208
305
|
<GridItem col={6} xs={12}>
|
|
209
306
|
<Select
|
|
210
307
|
name="moderatorRoles"
|
|
@@ -214,6 +311,7 @@ const Settings = () => {
|
|
|
214
311
|
onClear={() => setFieldValue('moderatorRoles', [], false)}
|
|
215
312
|
value={values.moderatorRoles}
|
|
216
313
|
onChange={(value) => setFieldValue('moderatorRoles', value, false)}
|
|
314
|
+
disabled={restartRequired}
|
|
217
315
|
multi
|
|
218
316
|
withTags
|
|
219
317
|
>
|
|
@@ -230,6 +328,7 @@ const Settings = () => {
|
|
|
230
328
|
onChange={({ target: { checked } }) => setFieldValue('badWords', checked, false)}
|
|
231
329
|
onLabel={getMessage('compontents.toogle.enabled')}
|
|
232
330
|
offLabel={getMessage('compontents.toogle.disabled')}
|
|
331
|
+
disabled={restartRequired}
|
|
233
332
|
/>
|
|
234
333
|
</GridItem>
|
|
235
334
|
{ isGQLPluginEnabled && (<GridItem col={6} xs={12}>
|
|
@@ -241,67 +340,18 @@ const Settings = () => {
|
|
|
241
340
|
onChange={({ target: { checked } }) => setFieldValue('gqlAuthEnabled', checked, false)}
|
|
242
341
|
onLabel={getMessage('compontents.toogle.enabled')}
|
|
243
342
|
offLabel={getMessage('compontents.toogle.disabled')}
|
|
343
|
+
disabled={restartRequired}
|
|
244
344
|
/>
|
|
245
345
|
</GridItem>) }
|
|
246
346
|
</Grid>
|
|
247
347
|
</Stack>
|
|
248
348
|
</Box>
|
|
249
|
-
{ !isEmpty(values.enabledCollections) && (<Box {...boxDefaultProps}>
|
|
250
|
-
<Stack size={4}>
|
|
251
|
-
<Typography variant="delta" as="h2">
|
|
252
|
-
{getMessage('page.settings.section.collections')}
|
|
253
|
-
</Typography>
|
|
254
|
-
<Grid gap={4}>
|
|
255
|
-
{ orderBy(values.enabledCollections).map(uid => {
|
|
256
|
-
const { schema: { displayName, attributes = {} } } = allCollections.find(_ => _.uid === uid);
|
|
257
|
-
const stringAttributes = Object.keys(attributes).filter(_ => attributes[_].type === 'string');
|
|
258
|
-
return (<GridItem key={`collectionSettings-${uid}`} col={6} s={12} xs={12}>
|
|
259
|
-
<Card background="primary100" borderColor="primary200">
|
|
260
|
-
<CardBody>
|
|
261
|
-
<CardContent style={{ width: '100%' }}>
|
|
262
|
-
<Stack size={4}>
|
|
263
|
-
<Typography variant="epsilon" fontWeight="semibold" as="h3">{ displayName }</Typography>
|
|
264
|
-
<ToggleInput
|
|
265
|
-
name={`collectionSettings-${uid}-approvalFlow`}
|
|
266
|
-
label={getMessage('page.settings.form.approvalFlow.label')}
|
|
267
|
-
hind={getMessage({
|
|
268
|
-
id: 'page.settings.form.approvalFlow.hint',
|
|
269
|
-
params: { name: displayName },
|
|
270
|
-
})}
|
|
271
|
-
checked={values.approvalFlow.includes(uid)}
|
|
272
|
-
onChange={({ target: { checked } }) => setFieldValue('approvalFlow', changeApprovalFlowFor(uid, values.approvalFlow, checked), [])}
|
|
273
|
-
onLabel={getMessage('compontents.toogle.enabled')}
|
|
274
|
-
offLabel={getMessage('compontents.toogle.disabled')}
|
|
275
|
-
/>
|
|
276
|
-
{ !isEmpty(stringAttributes) && (<Select
|
|
277
|
-
name={`collectionSettings-${uid}-entryLabel`}
|
|
278
|
-
label={getMessage('page.settings.form.entryLabel.label')}
|
|
279
|
-
placeholder={getMessage('page.settings.form.entryLabel.placeholder')}
|
|
280
|
-
hint={getMessage('page.settings.form.entryLabel.hint')}
|
|
281
|
-
onClear={() => setFieldValue('entryLabel', changeEntryLabelFor(uid, values.entryLabel))}
|
|
282
|
-
value={values.entryLabel[uid] || []}
|
|
283
|
-
onChange={(value) => setFieldValue('entryLabel', changeEntryLabelFor(uid, values.entryLabel, value))}
|
|
284
|
-
multi
|
|
285
|
-
withTags
|
|
286
|
-
>
|
|
287
|
-
{ stringAttributes.map(key =>
|
|
288
|
-
(<Option key={`collectionSettings-${uid}-entryLabel-${key}`} value={key}>{ capitalize(key.split('_').join(' ')) }</Option>))}
|
|
289
|
-
</Select>) }
|
|
290
|
-
</Stack>
|
|
291
|
-
</CardContent>
|
|
292
|
-
</CardBody>
|
|
293
|
-
</Card>
|
|
294
|
-
</GridItem>);
|
|
295
|
-
})}
|
|
296
|
-
</Grid>
|
|
297
|
-
</Stack>
|
|
298
|
-
</Box>)}
|
|
299
349
|
|
|
300
350
|
<CheckPermissions permissions={pluginPermissions.settingsChange}>
|
|
301
351
|
<Box {...boxDefaultProps}>
|
|
302
352
|
<Stack size={4}>
|
|
303
353
|
<Stack size={2}>
|
|
304
|
-
<Typography variant="delta"
|
|
354
|
+
<Typography variant="delta" as="h2">
|
|
305
355
|
{getMessage('page.settings.section.restore')}
|
|
306
356
|
</Typography>
|
|
307
357
|
<Typography variant="pi"as="h4">
|
|
@@ -7,7 +7,7 @@ export const fetchConfig = async (toggleNotification) => {
|
|
|
7
7
|
|
|
8
8
|
return data;
|
|
9
9
|
} catch (err) {
|
|
10
|
-
handleAPIError(err, toggleNotification);
|
|
10
|
+
handleAPIError(err, toggleNotification, 'page.settings.notification.fetch.error');
|
|
11
11
|
}
|
|
12
12
|
};
|
|
13
13
|
|
|
@@ -43,6 +43,16 @@ export const fetchAllContentTypes = async (toggleNotification) => {
|
|
|
43
43
|
export const fetchRoles = async (toggleNotification) => {
|
|
44
44
|
try {
|
|
45
45
|
const { data } = await axiosInstance.get(`/admin/roles`);
|
|
46
|
+
return data;
|
|
47
|
+
} catch (err) {
|
|
48
|
+
handleAPIError(err, toggleNotification);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const restartStrapi = async (toggleNotification) => {
|
|
53
|
+
try {
|
|
54
|
+
const { data } = await axiosInstance.get(getApiURL(`settings/restart`));
|
|
55
|
+
|
|
46
56
|
return data;
|
|
47
57
|
} catch (err) {
|
|
48
58
|
handleAPIError(err, toggleNotification);
|
|
@@ -85,14 +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
|
+
"page.settings.actions.restart": "Restart Strapi",
|
|
89
90
|
"page.settings.section.general": "General configuration",
|
|
90
|
-
"page.settings.section.
|
|
91
|
-
"page.settings.section.restore": "Restore default
|
|
91
|
+
"page.settings.section.additional": "Additional configuration",
|
|
92
|
+
"page.settings.section.restore": "Restore default settings",
|
|
92
93
|
"page.settings.section.restore.subtitle": "Discarding all of applied settings and getting back to plugin default ones. Use reasonable.",
|
|
93
|
-
"page.settings.form.enabledCollections.label": "Enable
|
|
94
|
+
"page.settings.form.enabledCollections.label": "Enable comments only for",
|
|
94
95
|
"page.settings.form.enabledCollections.placeholder": "Select one or more collection",
|
|
95
|
-
"page.settings.form.enabledCollections.hint": "If none
|
|
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",
|
|
96
99
|
"page.settings.form.moderatorRoles.label": "Send significant notifications to",
|
|
97
100
|
"page.settings.form.moderatorRoles.placeholder": "Select one or more roles",
|
|
98
101
|
"page.settings.form.moderatorRoles.hint": "Roles which are going to be notified by the plugin about significant actions to perform",
|
|
@@ -100,18 +103,24 @@
|
|
|
100
103
|
"page.settings.form.badWords.hint": "If enabled, every post / update of comment is going to be checked against bad wording",
|
|
101
104
|
"page.settings.form.gqlAuth.label": "GraphQL queries authorization",
|
|
102
105
|
"page.settings.form.gqlAuth.hint": "If enabled, GraphQL API queries & mutations can be triggered only by Authenticated Strapi users. Otherwise API remains open.",
|
|
103
|
-
"page.settings.form.approvalFlow.label": "
|
|
104
|
-
"page.settings.form.approvalFlow.hint": "Comments
|
|
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",
|
|
105
108
|
"page.settings.form.entryLabel.label": "Title fields",
|
|
106
109
|
"page.settings.form.entryLabel.placeholder": "Select at least one or leave empty to apply defaults",
|
|
107
110
|
"page.settings.form.entryLabel.hint": "If left empty title rendering is going to take following ordered fields: \"Title\", \"Subject\" & \"Name\"",
|
|
108
111
|
"page.settings.actions.restore.confirmation.header": "Restore default configuration",
|
|
109
112
|
"page.settings.actions.restore.confirmation.description": "You're about to restore plugin configuration to it default values. It might have destructive impact on already collected content. Do you really want to proceed?",
|
|
110
113
|
"page.settings.actions.restore.confirmation.button.confirm": "Yes, I want to restore",
|
|
114
|
+
"page.settings.actions.restart.alert.title": "Strapi requires restart",
|
|
115
|
+
"page.settings.actions.restart.alert.description": "You've made a configuration changes which requires your Strapi application to be restarted to take an effect in GraphQL schema. Do it manually or by using below trigger.",
|
|
116
|
+
"page.settings.actions.restart.alert.close": "Discard",
|
|
117
|
+
"page.settings.notification.fetch.error": "Failed to fetch configuration. Retrying...",
|
|
111
118
|
"page.settings.notification.submit.success": "Configuration has been saved successfully",
|
|
112
119
|
"page.settings.notification.submit.error": "Failed to save configuration. Try again.",
|
|
113
120
|
"page.settings.notification.restore.success": "Configuration has been restored successfully",
|
|
114
121
|
"page.settings.notification.restore.error": "Failed to restore configuration. Try again.",
|
|
122
|
+
"page.settings.notification.restart.success": "Application has been restarted successfully",
|
|
123
|
+
"page.settings.notification.restart.error": "Failed to restart your application. Try to do it manually.",
|
|
115
124
|
"page.settings.loading": "Fetching configuration...",
|
|
116
125
|
"compontents.confirmation.dialog.header": "Confirmation",
|
|
117
126
|
"compontents.confirmation.dialog.description": "Do you really want to perform this action?",
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import pluginId from "../pluginId";
|
|
1
2
|
|
|
2
|
-
const handleAPIError = (err = null, toggleNotification = null) => {
|
|
3
|
+
const handleAPIError = (err = null, toggleNotification = null, message = 'app.components.notification.error') => {
|
|
3
4
|
toggleNotification({
|
|
4
5
|
type: 'warning',
|
|
5
|
-
message:
|
|
6
|
+
message: `${pluginId}.${message}`,
|
|
6
7
|
});
|
|
7
8
|
|
|
8
9
|
if (err) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "strapi-plugin-comments",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.7",
|
|
4
4
|
"description": "Strapi - Comments plugin",
|
|
5
5
|
"strapi": {
|
|
6
6
|
"name": "comments",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"prepare": "husky install",
|
|
13
|
-
"publish": "npm publish --tag latest",
|
|
13
|
+
"publish:latest": "npm publish --tag latest",
|
|
14
14
|
"test:unit": "jest --verbose --coverage"
|
|
15
15
|
},
|
|
16
16
|
"repository": {
|
|
@@ -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
|
|
22
|
-
"@strapi/plugin-graphql": "^4.0
|
|
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": {
|
|
@@ -43,6 +43,15 @@ module.exports = {
|
|
|
43
43
|
}
|
|
44
44
|
},
|
|
45
45
|
|
|
46
|
+
async settingsRestart(ctx) {
|
|
47
|
+
try {
|
|
48
|
+
await this.getService().restart();
|
|
49
|
+
return ctx.send({ status: 200 });
|
|
50
|
+
} catch (e) {
|
|
51
|
+
throwError(ctx, e);
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
|
|
46
55
|
async findAll(ctx) {
|
|
47
56
|
return this.getService()
|
|
48
57
|
.findAll(ctx.query);
|
|
@@ -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,
|
|
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,
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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:
|
|
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));
|
package/server/routes/admin.js
CHANGED
|
@@ -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,134 @@ 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],
|
|
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', 3, 'content'], db[3].content);
|
|
331
|
+
expect(result).toHaveProperty(['meta', 'pagination', 'page'], 1);
|
|
332
|
+
expect(result).toHaveProperty(['meta', 'pagination', 'pageSize'], 5);
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
describe('findAllInHierarchy', () => {
|
|
337
|
+
test('Should return nested structure starting for root', async () => {
|
|
338
|
+
const result = await getPluginService('common').findAllInHierarchy({ query: { related }}, relatedEntity);
|
|
339
|
+
expect(result).not.toHaveProperty('data');
|
|
340
|
+
expect(result).not.toHaveProperty('meta');
|
|
341
|
+
expect(result.length).toBe(2);
|
|
342
|
+
expect(result).toHaveProperty([0, 'content'], db[0].content);
|
|
343
|
+
expect(result).toHaveProperty([0, 'children']);
|
|
344
|
+
expect(result[0].children.length).toBe(1);
|
|
345
|
+
expect(result).toHaveProperty([0, 'children', 0, 'content'], 'DEF');
|
|
346
|
+
expect(result).toHaveProperty([0, 'children', 0, 'children']);
|
|
347
|
+
expect(result[0].children[0].children.length).toBe(1);
|
|
348
|
+
expect(result).toHaveProperty([0, 'children', 0, 'children', 0, 'content'], 'IKL');
|
|
349
|
+
expect(result).toHaveProperty([1, 'content'], db[2].content);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test('Should return nested structure starting for comment id: 1', async () => {
|
|
353
|
+
const result = await getPluginService('common').findAllInHierarchy({ query: { related }, startingFromId: 1}, relatedEntity);
|
|
354
|
+
expect(result).not.toHaveProperty('data');
|
|
355
|
+
expect(result).not.toHaveProperty('meta');
|
|
356
|
+
expect(result.length).toBe(1);
|
|
357
|
+
expect(result).toHaveProperty([0, 'content'], db[1].content);
|
|
358
|
+
expect(result).toHaveProperty([0, 'children']);
|
|
359
|
+
expect(result[0].children.length).toBe(1);
|
|
360
|
+
expect(result).toHaveProperty([0, 'children', 0, 'content'], 'IKL');
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test('Should return nested structure starting for comment id: 1 without blocked comments', async () => {
|
|
364
|
+
const result = await getPluginService('common').findAllInHierarchy({ query: { related }, startingFromId: 1, dropBlockedThreads: true}, relatedEntity);
|
|
365
|
+
expect(result).not.toHaveProperty('data');
|
|
366
|
+
expect(result).not.toHaveProperty('meta');
|
|
367
|
+
expect(result.length).toBe(1);
|
|
368
|
+
expect(result).toHaveProperty([0, 'content'], db[1].content);
|
|
369
|
+
expect(result).toHaveProperty([0, 'children']);
|
|
370
|
+
expect(result[0].children.length).toBe(0);
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
});
|
|
245
374
|
});
|
|
246
375
|
});
|
package/server/services/admin.js
CHANGED
|
@@ -75,19 +75,16 @@ const { APPROVAL_STATUS, REGEX } = require('./../utils/constants')
|
|
|
75
75
|
|
|
76
76
|
async restoreConfig() {
|
|
77
77
|
const pluginStore = await this.getCommonService().getPluginStore();
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
|
|
80
79
|
await pluginStore.delete({key: 'config'})
|
|
81
|
-
await pluginStore.set({
|
|
82
|
-
key: 'config',
|
|
83
|
-
value: {
|
|
84
|
-
...defaultConfig,
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
80
|
|
|
88
81
|
return this.config();
|
|
89
82
|
},
|
|
90
83
|
|
|
84
|
+
async restart() {
|
|
85
|
+
setImmediate(() => strapi.reload());
|
|
86
|
+
},
|
|
87
|
+
|
|
91
88
|
// Find all comments
|
|
92
89
|
async findAll({ related, entity, ...query }) {
|
|
93
90
|
const { _q, orderBy, pageSize = 10, page = 1, filters, ...rest } = parseParams(query);
|
|
@@ -38,7 +38,8 @@ module.exports = ({ strapi }) => ({
|
|
|
38
38
|
throw new PluginError(400, `Relation for field "related" does not exist. Check your payload please.`);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
const
|
|
41
|
+
const approvalFlow = await this.getCommonService().getConfig(CONFIG_PARAMS.APPROVAL_FLOW, []);
|
|
42
|
+
const isApprovalFlowEnabled = approvalFlow.includes(uid) ||
|
|
42
43
|
relatedEntity.requireCommentsApproval;
|
|
43
44
|
|
|
44
45
|
const linkToThread = !isNil(threadOf) ? await this.getCommonService().findOne({
|
|
@@ -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 {
|
|
@@ -26,7 +26,7 @@ module.exports = ({ strapi }) => ({
|
|
|
26
26
|
|
|
27
27
|
let result;
|
|
28
28
|
if (config && !useLocal) {
|
|
29
|
-
result = queryProp ? get(config, queryProp) : config
|
|
29
|
+
result = queryProp ? get(config, queryProp, defaultValue) : config;
|
|
30
30
|
} else {
|
|
31
31
|
result = this.getLocalConfig(queryProp, defaultValue);
|
|
32
32
|
}
|
|
@@ -68,12 +68,35 @@ module.exports = ({ strapi }) => ({
|
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
let meta = {};
|
|
71
72
|
if (pagination && isObject(pagination)) {
|
|
72
|
-
const
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|