strapi-plugin-comments 2.0.2 → 2.0.3
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/.husky/pre-commit +4 -0
- package/README.md +14 -1
- package/__mocks__/initSetup.js +31 -2
- package/admin/src/hooks/useConfig.js +43 -0
- package/admin/src/index.js +31 -2
- package/admin/src/pages/Details/index.js +5 -2
- package/admin/src/pages/Settings/index.js +342 -0
- package/admin/src/pages/Settings/utils/api.js +50 -0
- package/admin/src/permissions.js +3 -0
- package/admin/src/translations/en.json +37 -1
- package/admin/src/utils/index.js +2 -1
- package/admin/src/utils/parseRegExp.js +10 -0
- package/package.json +7 -4
- package/server/bootstrap.js +24 -0
- package/server/config/index.js +1 -0
- package/server/controllers/admin.js +33 -8
- package/server/graphql/index.js +7 -8
- package/server/graphql/resolvers-config.js +2 -4
- package/server/graphql/types/report-reason.js +10 -10
- package/server/register.js +1 -3
- package/server/routes/admin.js +24 -0
- package/server/services/__tests__/common.test.js +95 -14
- package/server/services/admin.js +60 -6
- package/server/services/client.js +13 -8
- package/server/services/common.js +37 -9
- package/server/services/utils/__tests__/functions.test.js +12 -1
- package/server/services/utils/functions.js +9 -1
- package/server/utils/constants.js +9 -0
- package/strapi-server.js +12 -5
package/README.md
CHANGED
|
@@ -92,6 +92,16 @@ In our minimum support we're following [official Node.js releases timelines](htt
|
|
|
92
92
|
**We recommend always using the latest version of Strapi to start your new projects**.
|
|
93
93
|
|
|
94
94
|
## 🔧 Configuration
|
|
95
|
+
|
|
96
|
+
### In `v2.0.3` and newest
|
|
97
|
+
|
|
98
|
+
Version `2.0.3` introduce the intuitive **Settings** page which you can easly access via `Strapi Settings -> Section: Comments Plugin -> Configuration`. On dedicated page you will be able to setup all crucial properties which drives the plugin and customize each individual collection for which **Comments plugin** should be enabled.
|
|
99
|
+
|
|
100
|
+
> *Note*
|
|
101
|
+
> Default configuration for your plugin is fetched from `config/plugins.js` or directly from the plugin itself. If you would like to customize the default state to which you might revert, please follow the next section.
|
|
102
|
+
|
|
103
|
+
### In `v2.0.2` and older + default configuration state for `v2.0.3` and newest
|
|
104
|
+
|
|
95
105
|
To setup amend default plugin configuration we recommend to put following snippet as part of `config/plugins.js` or `config/<env>/plugins.js` file. If the file does not exist yet, you have to create it manually. If you've got already configurations for other plugins stores by this way, use just the `comments` part within exising `plugins` item.
|
|
96
106
|
|
|
97
107
|
|
|
@@ -128,6 +138,9 @@ To setup amend default plugin configuration we recommend to put following snippe
|
|
|
128
138
|
## Additional GQL Configuration
|
|
129
139
|
All you need to do is to install and enable `@strapi/plugin-graphql` for you instance based on the **[official Strapi v4 docs](https://docs.strapi.io/developer-docs/latest/plugins/graphql.html#configurations)** and decide if you would like to call it by anyone (open for world) or only by authenticated users (Strapi users).
|
|
130
140
|
|
|
141
|
+
> **Important!**
|
|
142
|
+
> If you're using `config/plugins.js` to configure your plugins , please put `comments` property before `graphql`. Otherwise types are not going to be properly added to GraphQL Schema. That's because of dynamic types which base on plugin configuration which are added on `boostrap` stage, not `register`. This is not valid if you're using `graphql` plugin without any custom configuration, so most of cases in real.
|
|
143
|
+
|
|
131
144
|
```json
|
|
132
145
|
{
|
|
133
146
|
// ...
|
|
@@ -360,7 +373,7 @@ Updates a specified Comment content based on it `commentId` and related to speci
|
|
|
360
373
|
|
|
361
374
|
*GraphQL equivalent: [Public GraphQL API -> Delete Comment](#delete-comment-1)*
|
|
362
375
|
|
|
363
|
-
`DELETE <host>/api/comments/api::<collection name>.<content type name>:<entity id
|
|
376
|
+
`DELETE <host>/api/comments/api::<collection name>.<content type name>:<entity id>/comment/<commentId>?authorId=<authorId>`
|
|
364
377
|
|
|
365
378
|
Deletes a specified Comment based on it `commentId` and related to specified instance of Content Type like for example `Page` with `ID: 1`.
|
|
366
379
|
|
package/__mocks__/initSetup.js
CHANGED
|
@@ -1,13 +1,42 @@
|
|
|
1
1
|
const { get, set } = require('lodash');
|
|
2
2
|
|
|
3
|
-
module.exports = (config = {}) => {
|
|
3
|
+
module.exports = (config = {}, toStore = false) => {
|
|
4
4
|
const mock = {
|
|
5
|
+
db: toStore ? {
|
|
6
|
+
plugin: {
|
|
7
|
+
comments: {
|
|
8
|
+
config: { ...config },
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
}: {},
|
|
5
12
|
getRef: function() {
|
|
6
13
|
return this;
|
|
7
14
|
},
|
|
8
15
|
plugin: function(name) {
|
|
9
16
|
return this.plugins[name];
|
|
10
17
|
},
|
|
18
|
+
store: async function(storeProps) {
|
|
19
|
+
const { type, name } = storeProps; // { type: 'plugin', name: 'comments' }
|
|
20
|
+
|
|
21
|
+
const mockedStore = {
|
|
22
|
+
get: async function(props) { // { key: 'config' }
|
|
23
|
+
const { key } = props;
|
|
24
|
+
return new Promise(resolve => resolve(get(mock.db, `${type}.${name}.${key}`, undefined)));
|
|
25
|
+
},
|
|
26
|
+
set: async function(props) { // { key: 'config', value: {...} }
|
|
27
|
+
const { key, value } = props;
|
|
28
|
+
set(mock.db, `${type}.${name}.${key}`, value);
|
|
29
|
+
return this.get(key);
|
|
30
|
+
},
|
|
31
|
+
delete: async function(props) { // { key: 'config' }
|
|
32
|
+
const { key } = props;
|
|
33
|
+
set(mock.db, `${type}.${name}.${key}`, undefined);
|
|
34
|
+
return new Promise(resolve => resolve(true));
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return new Promise(resolve => resolve(mockedStore));
|
|
39
|
+
},
|
|
11
40
|
plugins: {
|
|
12
41
|
comments: {
|
|
13
42
|
service: function(name) {
|
|
@@ -44,7 +73,7 @@ module.exports = (config = {}) => {
|
|
|
44
73
|
entryLabel: {
|
|
45
74
|
'api::blog-post.blog-post': ['alternative_subject'],
|
|
46
75
|
},
|
|
47
|
-
...config
|
|
76
|
+
...(toStore ? {} : config)
|
|
48
77
|
}
|
|
49
78
|
}
|
|
50
79
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useQuery, useMutation, useQueryClient } from 'react-query';
|
|
2
|
+
import { fetchConfig, restoreConfig, updateConfig } from '../pages/Settings/utils/api';
|
|
3
|
+
import pluginId from '../pluginId';
|
|
4
|
+
import { getMessage } from '../utils';
|
|
5
|
+
|
|
6
|
+
const useConfig = (toggleNotification) => {
|
|
7
|
+
const queryClient = useQueryClient();
|
|
8
|
+
|
|
9
|
+
const fetch = useQuery('get-config', () =>
|
|
10
|
+
fetchConfig(toggleNotification)
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
const handleError = (type, callback = () => {}) => {
|
|
14
|
+
toggleNotification({
|
|
15
|
+
type: 'warning',
|
|
16
|
+
message: `${pluginId}.page.settings.notification.${type}.error`,
|
|
17
|
+
});
|
|
18
|
+
callback();
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const handleSuccess = (type, callback = () => {}) => {
|
|
22
|
+
queryClient.invalidateQueries('get-config');
|
|
23
|
+
toggleNotification({
|
|
24
|
+
type: 'success',
|
|
25
|
+
message: `${pluginId}.page.settings.notification.${type}.success`,
|
|
26
|
+
});
|
|
27
|
+
callback();
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const submitMutation = useMutation(updateConfig, {
|
|
31
|
+
onSuccess: () => handleSuccess('submit'),
|
|
32
|
+
onError: () => handleError('submit'),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const restoreMutation = useMutation(restoreConfig, {
|
|
36
|
+
onSuccess: () => handleSuccess('restore'),
|
|
37
|
+
onError: () => handleError('restore'),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return { fetch, submitMutation, restoreMutation };
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export default useConfig;
|
package/admin/src/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import PluginIcon from './components/PluginIcon';
|
|
|
6
6
|
import pluginPermissions from './permissions';
|
|
7
7
|
import reducers from './reducers';
|
|
8
8
|
|
|
9
|
-
const name = pluginPkg.strapi
|
|
9
|
+
const { name, displayName } = pluginPkg.strapi;
|
|
10
10
|
|
|
11
11
|
export default {
|
|
12
12
|
register(app) {
|
|
@@ -16,7 +16,7 @@ export default {
|
|
|
16
16
|
icon: PluginIcon,
|
|
17
17
|
intlLabel: {
|
|
18
18
|
id: `${pluginId}.plugin.name`,
|
|
19
|
-
defaultMessage:
|
|
19
|
+
defaultMessage: displayName,
|
|
20
20
|
},
|
|
21
21
|
Component: async () => {
|
|
22
22
|
const component = await import(/* webpackChunkName: "[request]" */ './pages/App');
|
|
@@ -25,6 +25,35 @@ export default {
|
|
|
25
25
|
},
|
|
26
26
|
permissions: pluginPermissions.access,
|
|
27
27
|
});
|
|
28
|
+
|
|
29
|
+
app.createSettingSection(
|
|
30
|
+
{
|
|
31
|
+
id: pluginId,
|
|
32
|
+
intlLabel: {
|
|
33
|
+
id: `${pluginId}.plugin.section`,
|
|
34
|
+
defaultMessage: `${displayName} plugin`,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
[
|
|
38
|
+
{
|
|
39
|
+
intlLabel: {
|
|
40
|
+
id: `${pluginId}.plugin.section.item`,
|
|
41
|
+
defaultMessage: 'Configuration',
|
|
42
|
+
},
|
|
43
|
+
id: 'comments',
|
|
44
|
+
to: `/settings/${pluginId}`,
|
|
45
|
+
Component: async () => {
|
|
46
|
+
const component = await import(
|
|
47
|
+
/* webpackChunkName: "documentation-settings" */ './pages/Settings'
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
return component;
|
|
51
|
+
},
|
|
52
|
+
permissions: pluginPermissions.settings,
|
|
53
|
+
},
|
|
54
|
+
]
|
|
55
|
+
);
|
|
56
|
+
|
|
28
57
|
app.addReducers(reducers);
|
|
29
58
|
app.registerPlugin({
|
|
30
59
|
id: pluginId,
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
import getMessage from '../../utils/getMessage';
|
|
29
29
|
import { fetchDetailsData, fetchContentTypeData } from '../utils/api';
|
|
30
30
|
import pluginPermissions from '../../permissions';
|
|
31
|
-
import { getUrl } from '../../utils';
|
|
31
|
+
import { getUrl, parseRegExp } from '../../utils';
|
|
32
32
|
import Nav from '../../components/Nav';
|
|
33
33
|
import DetailsEntity from './components/DetailsEntity';
|
|
34
34
|
import DiscussionThread from '../../components/DiscussionThread';
|
|
@@ -60,7 +60,10 @@ const Details = ({ config }) => {
|
|
|
60
60
|
|
|
61
61
|
const [filters, setFilters] = useState({});
|
|
62
62
|
|
|
63
|
-
const regexUID = new RegExp(
|
|
63
|
+
const regexUID = new RegExp(
|
|
64
|
+
parseRegExp(config.regex.uid).value,
|
|
65
|
+
parseRegExp(config.regex.uid).flags
|
|
66
|
+
);
|
|
64
67
|
|
|
65
68
|
const { isLoading: isLoadingForData, data, isFetching } = useQuery(
|
|
66
69
|
['get-details-data', id, filters, canAccess],
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import React, { useRef, useMemo, useState } from 'react';
|
|
2
|
+
import { useQuery } from 'react-query';
|
|
3
|
+
import { Formik } from 'formik';
|
|
4
|
+
import { capitalize, first, orderBy, isEmpty } from 'lodash';
|
|
5
|
+
import {
|
|
6
|
+
CheckPermissions,
|
|
7
|
+
LoadingIndicatorPage,
|
|
8
|
+
Form,
|
|
9
|
+
useTracking,
|
|
10
|
+
useNotification,
|
|
11
|
+
useRBAC,
|
|
12
|
+
useFocusWhenNavigate,
|
|
13
|
+
useOverlayBlocker,
|
|
14
|
+
} from '@strapi/helper-plugin';
|
|
15
|
+
import { Main } from '@strapi/design-system/Main';
|
|
16
|
+
import { ContentLayout, HeaderLayout } from '@strapi/design-system/Layout';
|
|
17
|
+
import { Button } from '@strapi/design-system/Button';
|
|
18
|
+
import { Box } from '@strapi/design-system/Box';
|
|
19
|
+
import { Stack } from '@strapi/design-system/Stack';
|
|
20
|
+
import { Typography } from '@strapi/design-system/Typography';
|
|
21
|
+
import { Grid, GridItem } from '@strapi/design-system/Grid';
|
|
22
|
+
import { ToggleInput } from '@strapi/design-system/ToggleInput';
|
|
23
|
+
import { Select, Option } from '@strapi/design-system/Select';
|
|
24
|
+
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 } from '@strapi/icons';
|
|
31
|
+
|
|
32
|
+
import pluginPermissions from '../../permissions';
|
|
33
|
+
import useConfig from '../../hooks/useConfig';
|
|
34
|
+
import { fetchAllContentTypes, fetchRoles } from './utils/api';
|
|
35
|
+
import { getMessage, parseRegExp } from '../../utils';
|
|
36
|
+
import ConfirmationDialog from '../../components/ConfirmationDialog';
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
const Settings = () => {
|
|
40
|
+
useFocusWhenNavigate();
|
|
41
|
+
|
|
42
|
+
const { notifyStatus } = useNotifyAT();
|
|
43
|
+
const { trackUsage } = useTracking();
|
|
44
|
+
const trackUsageRef = useRef(trackUsage);
|
|
45
|
+
const toggleNotification = useNotification();
|
|
46
|
+
const { lockApp, unlockApp } = useOverlayBlocker();
|
|
47
|
+
|
|
48
|
+
const viewPermissions = useMemo(() => ({
|
|
49
|
+
access: pluginPermissions.settings.read,
|
|
50
|
+
change: pluginPermissions.settings.change,
|
|
51
|
+
}), []);
|
|
52
|
+
|
|
53
|
+
const {
|
|
54
|
+
isLoading: isLoadingForPermissions,
|
|
55
|
+
allowedActions: { canAccess, canChange },
|
|
56
|
+
} = useRBAC(viewPermissions);
|
|
57
|
+
|
|
58
|
+
const [restoreConfigmationVisible, setRestoreConfigmationVisible] = useState(false);
|
|
59
|
+
|
|
60
|
+
const { fetch, submitMutation, restoreMutation } = useConfig(toggleNotification);
|
|
61
|
+
const { data: configData, isLoading: isConfigLoading, err: configErr } = fetch;
|
|
62
|
+
|
|
63
|
+
const { data: allCollectionsData, isLoading: areCollectionsLoading, err: collectionsErr } = useQuery(
|
|
64
|
+
['get-all-content-types', canAccess],
|
|
65
|
+
() => fetchAllContentTypes(toggleNotification)
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const { data: allRolesData, isLoading: areRolesLoading, err: rolesErr } = useQuery(
|
|
69
|
+
['get-all-roles', canAccess],
|
|
70
|
+
() => fetchRoles(toggleNotification)
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const isLoading = isLoadingForPermissions || isConfigLoading || areCollectionsLoading || areRolesLoading;
|
|
74
|
+
const isError = configErr || collectionsErr || rolesErr;
|
|
75
|
+
|
|
76
|
+
const preparePayload = ({ enabledCollections, gqlAuthEnabled, approvalFlow, entryLabel, ...rest }) => ({
|
|
77
|
+
...rest,
|
|
78
|
+
enabledCollections,
|
|
79
|
+
approvalFlow: approvalFlow.filter(_ => enabledCollections.includes(_)),
|
|
80
|
+
entryLabel: {
|
|
81
|
+
...Object.keys(entryLabel).reduce((prev, curr) => ({
|
|
82
|
+
...prev,
|
|
83
|
+
[curr]: enabledCollections.includes(curr) ? entryLabel[curr] : undefined,
|
|
84
|
+
}), {}),
|
|
85
|
+
'*': entryLabel['*'],
|
|
86
|
+
},
|
|
87
|
+
reportReasons: configData?.reportReasons,
|
|
88
|
+
gql: gqlAuthEnabled ? { auth: true } : undefined,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
if (isLoading || isError) {
|
|
92
|
+
return (<LoadingIndicatorPage>
|
|
93
|
+
{ getMessage('page.settings.loading')}
|
|
94
|
+
</LoadingIndicatorPage>);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const regexUID = !isLoading ? new RegExp(
|
|
98
|
+
parseRegExp(fetch.data.regex?.uid).value,
|
|
99
|
+
parseRegExp(fetch.data.regex?.uid).flags
|
|
100
|
+
) : null;
|
|
101
|
+
|
|
102
|
+
const allRoles = allRolesData?.data || [];
|
|
103
|
+
const allCollections = !isLoading && allCollectionsData
|
|
104
|
+
.filter(({ uid }) => first(uid.split(regexUID).filter(s => s && s.length > 0)) === 'api');
|
|
105
|
+
const enabledCollections = configData?.enabledCollections
|
|
106
|
+
?.map(uid => allCollections.find(_ => _.uid === uid) ? uid : undefined)
|
|
107
|
+
.filter(_ => _) || [];
|
|
108
|
+
const entryLabel = configData?.entryLabel || {};
|
|
109
|
+
const approvalFlow = configData?.approvalFlow || [];
|
|
110
|
+
const badWords = configData?.badWords || undefined;
|
|
111
|
+
const isGQLPluginEnabled = configData?.isGQLPluginEnabled;
|
|
112
|
+
const gqlAuthEnabled = configData?.gql?.auth || undefined;
|
|
113
|
+
const moderatorRoles = configData?.moderatorRoles
|
|
114
|
+
?.filter(code => allRoles.find(_ => _.code === code)) || [];
|
|
115
|
+
|
|
116
|
+
const changeApprovalFlowFor = (uid, current, value) => {
|
|
117
|
+
const currentSet = new Set(current);
|
|
118
|
+
if (value) {
|
|
119
|
+
currentSet.add(uid);
|
|
120
|
+
} else {
|
|
121
|
+
currentSet.delete(uid);
|
|
122
|
+
}
|
|
123
|
+
return Array.from(currentSet);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const changeEntryLabelFor = (uid, current, value) => ({
|
|
127
|
+
...current,
|
|
128
|
+
[uid]: value && !isEmpty(value) ? [...value] : undefined,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const handleUpdateConfiguration = async (form) => {
|
|
132
|
+
if (canChange) {
|
|
133
|
+
lockApp();
|
|
134
|
+
await submitMutation.mutateAsync(preparePayload(form));
|
|
135
|
+
unlockApp();
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const handleRestoreConfirmation = () => setRestoreConfigmationVisible(true);
|
|
140
|
+
const handleRestoreConfiguration = async () => {
|
|
141
|
+
if (canChange) {
|
|
142
|
+
lockApp();
|
|
143
|
+
await restoreMutation.mutateAsync();
|
|
144
|
+
unlockApp();
|
|
145
|
+
setRestoreConfigmationVisible(false);
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
const handleRestoreCancel = () => setRestoreConfigmationVisible(false);
|
|
149
|
+
|
|
150
|
+
const boxDefaultProps = {
|
|
151
|
+
background: "neutral0",
|
|
152
|
+
hasRadius: true,
|
|
153
|
+
shadow: "filterShadow",
|
|
154
|
+
padding: 6,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<Main>
|
|
159
|
+
<Formik
|
|
160
|
+
initialValues={{
|
|
161
|
+
enabledCollections,
|
|
162
|
+
moderatorRoles,
|
|
163
|
+
badWords,
|
|
164
|
+
approvalFlow,
|
|
165
|
+
entryLabel,
|
|
166
|
+
gqlAuthEnabled,
|
|
167
|
+
}}
|
|
168
|
+
enableReinitialize={true}
|
|
169
|
+
onSubmit={handleUpdateConfiguration}
|
|
170
|
+
>
|
|
171
|
+
{({ handleSubmit, setFieldValue, values }) => (
|
|
172
|
+
<Form noValidate onSubmit={handleSubmit}>
|
|
173
|
+
<HeaderLayout
|
|
174
|
+
title={getMessage('page.settings.header.title')}
|
|
175
|
+
subtitle={getMessage('page.settings.header.description')}
|
|
176
|
+
primaryAction={
|
|
177
|
+
<CheckPermissions permissions={pluginPermissions.settingsChange}>
|
|
178
|
+
<Button type="submit" startIcon={<Check />} >
|
|
179
|
+
{getMessage('page.settings.actions.submit')}
|
|
180
|
+
</Button>
|
|
181
|
+
</CheckPermissions>
|
|
182
|
+
}
|
|
183
|
+
/>
|
|
184
|
+
<ContentLayout>
|
|
185
|
+
<Stack size={6}>
|
|
186
|
+
<Box {...boxDefaultProps}>
|
|
187
|
+
<Stack size={4}>
|
|
188
|
+
<Typography variant="delta" as="h2">
|
|
189
|
+
{getMessage('page.settings.section.general')}
|
|
190
|
+
</Typography>
|
|
191
|
+
<Grid gap={4}>
|
|
192
|
+
<GridItem col={12}>
|
|
193
|
+
<Select
|
|
194
|
+
name="enabledCollections"
|
|
195
|
+
label={getMessage('page.settings.form.enabledCollections.label')}
|
|
196
|
+
placeholder={getMessage('page.settings.form.enabledCollections.placeholder')}
|
|
197
|
+
hint={getMessage('page.settings.form.enabledCollections.hint')}
|
|
198
|
+
onClear={() => setFieldValue('enabledCollections', [], false)}
|
|
199
|
+
value={values.enabledCollections}
|
|
200
|
+
onChange={(value) => setFieldValue('enabledCollections', value, false)}
|
|
201
|
+
multi
|
|
202
|
+
withTags
|
|
203
|
+
>
|
|
204
|
+
{ allCollections.map(({ uid, schema: { displayName } }) =>
|
|
205
|
+
(<Option key={uid} value={uid}>{displayName}</Option>))}
|
|
206
|
+
</Select>
|
|
207
|
+
</GridItem>
|
|
208
|
+
<GridItem col={6} xs={12}>
|
|
209
|
+
<Select
|
|
210
|
+
name="moderatorRoles"
|
|
211
|
+
label={getMessage('page.settings.form.moderatorRoles.label')}
|
|
212
|
+
placeholder={getMessage('page.settings.form.moderatorRoles.placeholder')}
|
|
213
|
+
hint={getMessage('page.settings.form.moderatorRoles.hint')}
|
|
214
|
+
onClear={() => setFieldValue('moderatorRoles', [], false)}
|
|
215
|
+
value={values.moderatorRoles}
|
|
216
|
+
onChange={(value) => setFieldValue('moderatorRoles', value, false)}
|
|
217
|
+
multi
|
|
218
|
+
withTags
|
|
219
|
+
>
|
|
220
|
+
{ allRoles.map(({ code, name }) =>
|
|
221
|
+
(<Option key={code} value={code}>{name}</Option>))}
|
|
222
|
+
</Select>
|
|
223
|
+
</GridItem>
|
|
224
|
+
<GridItem col={6} xs={12}>
|
|
225
|
+
<ToggleInput
|
|
226
|
+
name="badWords"
|
|
227
|
+
label={getMessage('page.settings.form.badWords.label')}
|
|
228
|
+
hint={getMessage('page.settings.form.badWords.hint')}
|
|
229
|
+
checked={values.badWords}
|
|
230
|
+
onChange={({ target: { checked } }) => setFieldValue('badWords', checked, false)}
|
|
231
|
+
onLabel={getMessage('compontents.toogle.enabled')}
|
|
232
|
+
offLabel={getMessage('compontents.toogle.disabled')}
|
|
233
|
+
/>
|
|
234
|
+
</GridItem>
|
|
235
|
+
{ isGQLPluginEnabled && (<GridItem col={6} xs={12}>
|
|
236
|
+
<ToggleInput
|
|
237
|
+
name="badWords"
|
|
238
|
+
label={getMessage('page.settings.form.gqlAuth.label')}
|
|
239
|
+
hint={getMessage('page.settings.form.gqlAuth.hint')}
|
|
240
|
+
checked={values.gqlAuthEnabled}
|
|
241
|
+
onChange={({ target: { checked } }) => setFieldValue('gqlAuthEnabled', checked, false)}
|
|
242
|
+
onLabel={getMessage('compontents.toogle.enabled')}
|
|
243
|
+
offLabel={getMessage('compontents.toogle.disabled')}
|
|
244
|
+
/>
|
|
245
|
+
</GridItem>) }
|
|
246
|
+
</Grid>
|
|
247
|
+
</Stack>
|
|
248
|
+
</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
|
+
|
|
300
|
+
<CheckPermissions permissions={pluginPermissions.settingsChange}>
|
|
301
|
+
<Box {...boxDefaultProps}>
|
|
302
|
+
<Stack size={4}>
|
|
303
|
+
<Stack size={2}>
|
|
304
|
+
<Typography variant="delta" textColor="danger700" as="h2">
|
|
305
|
+
{getMessage('page.settings.section.restore')}
|
|
306
|
+
</Typography>
|
|
307
|
+
<Typography variant="pi"as="h4">
|
|
308
|
+
{getMessage('page.settings.section.restore.subtitle')}
|
|
309
|
+
</Typography>
|
|
310
|
+
</Stack>
|
|
311
|
+
<Grid gap={4}>
|
|
312
|
+
<GridItem col={6}>
|
|
313
|
+
<Button variant="danger-light" startIcon={<Refresh />} onClick={handleRestoreConfirmation}>
|
|
314
|
+
{getMessage('page.settings.actions.restore')}
|
|
315
|
+
</Button>
|
|
316
|
+
|
|
317
|
+
<ConfirmationDialog
|
|
318
|
+
isVisible={restoreConfigmationVisible}
|
|
319
|
+
isActionAsync={restoreMutation.isLoading}
|
|
320
|
+
header={getMessage('page.settings.actions.restore.confirmation.header')}
|
|
321
|
+
labelConfirm={getMessage('page.settings.actions.restore.confirmation.button.confirm')}
|
|
322
|
+
iconConfirm={<Refresh />}
|
|
323
|
+
onConfirm={handleRestoreConfiguration}
|
|
324
|
+
onCancel={handleRestoreCancel}>
|
|
325
|
+
{ getMessage('page.settings.actions.restore.confirmation.description') }
|
|
326
|
+
</ConfirmationDialog>
|
|
327
|
+
</GridItem>
|
|
328
|
+
</Grid>
|
|
329
|
+
</Stack>
|
|
330
|
+
</Box>
|
|
331
|
+
</CheckPermissions>
|
|
332
|
+
</Stack>
|
|
333
|
+
</ContentLayout>
|
|
334
|
+
</Form>
|
|
335
|
+
)}
|
|
336
|
+
</Formik>
|
|
337
|
+
</Main>
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
export default Settings;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { getApiURL, axiosInstance, handleAPIError } from '../../../utils';
|
|
2
|
+
|
|
3
|
+
// eslint-disable-next-line import/prefer-default-export
|
|
4
|
+
export const fetchConfig = async (toggleNotification) => {
|
|
5
|
+
try {
|
|
6
|
+
const { data } = await axiosInstance.get(getApiURL(`settings/config`));
|
|
7
|
+
|
|
8
|
+
return data;
|
|
9
|
+
} catch (err) {
|
|
10
|
+
handleAPIError(err, toggleNotification);
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const updateConfig = async (body, toggleNotification) => {
|
|
15
|
+
try {
|
|
16
|
+
const { data } = await axiosInstance.put(getApiURL(`settings/config`), body);
|
|
17
|
+
|
|
18
|
+
return data;
|
|
19
|
+
} catch (err) {
|
|
20
|
+
handleAPIError(err, toggleNotification);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const restoreConfig = async (toggleNotification) => {
|
|
25
|
+
try {
|
|
26
|
+
const { data } = await axiosInstance.delete(getApiURL(`settings/config`));
|
|
27
|
+
|
|
28
|
+
return data;
|
|
29
|
+
} catch (err) {
|
|
30
|
+
handleAPIError(err, toggleNotification);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const fetchAllContentTypes = async (toggleNotification) => {
|
|
35
|
+
try {
|
|
36
|
+
const { data } = await axiosInstance.get(`/content-type-builder/content-types`);
|
|
37
|
+
return data?.data;
|
|
38
|
+
} catch (err) {
|
|
39
|
+
handleAPIError(err, toggleNotification);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const fetchRoles = async (toggleNotification) => {
|
|
44
|
+
try {
|
|
45
|
+
const { data } = await axiosInstance.get(`/admin/roles`);
|
|
46
|
+
return data;
|
|
47
|
+
} catch (err) {
|
|
48
|
+
handleAPIError(err, toggleNotification);
|
|
49
|
+
}
|
|
50
|
+
};
|
package/admin/src/permissions.js
CHANGED
|
@@ -6,11 +6,14 @@ const pluginPermissions = {
|
|
|
6
6
|
{ action: permissions.render(permissions.reports.read), subject: null },
|
|
7
7
|
// { action: 'plugin::comments.discover.read', subject: null },
|
|
8
8
|
{ action: permissions.render(permissions.settings.read), subject: null },
|
|
9
|
+
{ action: permissions.render(permissions.settings.read), subject: null },
|
|
9
10
|
],
|
|
10
11
|
// discover: [{ action: 'plugin::comments.discover.read', subject: null }],
|
|
11
12
|
moderate: [{ action: permissions.render(permissions.comments.moderate), subject: null }],
|
|
12
13
|
reports: [{ action: permissions.render(permissions.reports.read), subject: null }],
|
|
13
14
|
reportsReview: [{ action: permissions.render(permissions.reports.action), subject: null }],
|
|
15
|
+
settings: [{ action: permissions.render(permissions.settings.read), subject: null }],
|
|
16
|
+
settingsChange: [{ action: permissions.render(permissions.settings.change), subject: null }],
|
|
14
17
|
};
|
|
15
18
|
|
|
16
19
|
export default pluginPermissions;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"plugin.name": "Comments",
|
|
3
|
+
"plugin.section": "Comments plugin",
|
|
4
|
+
"plugin.section.item": "Configuration",
|
|
3
5
|
"common.button.back": "Go back",
|
|
4
6
|
"nav.header.moderation": "Moderation",
|
|
5
7
|
"nav.header.settings": "Settings",
|
|
@@ -79,8 +81,42 @@
|
|
|
79
81
|
"page.details.filters.label": "View",
|
|
80
82
|
"page.details.filters.removed.visibility": "Show removed comments",
|
|
81
83
|
"page.coming.soon": "Wait for what is coming soon...",
|
|
84
|
+
"page.settings": "Settings",
|
|
85
|
+
"page.settings.header.title": "Comments",
|
|
86
|
+
"page.settings.header.description": "Configure your comments moderation capabilities",
|
|
87
|
+
"page.settings.actions.submit": "Save",
|
|
88
|
+
"page.settings.actions.restore": "Restore",
|
|
89
|
+
"page.settings.section.general": "General configuration",
|
|
90
|
+
"page.settings.section.collections": "Collection(s) configuration",
|
|
91
|
+
"page.settings.section.restore": "Restore default configuration",
|
|
92
|
+
"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 Comments for Collection(s)",
|
|
94
|
+
"page.settings.form.enabledCollections.placeholder": "Select one or more collection",
|
|
95
|
+
"page.settings.form.enabledCollections.hint": "If none of collection is selected, feature are enabled for any of exsiting",
|
|
96
|
+
"page.settings.form.moderatorRoles.label": "Send significant notifications to",
|
|
97
|
+
"page.settings.form.moderatorRoles.placeholder": "Select one or more roles",
|
|
98
|
+
"page.settings.form.moderatorRoles.hint": "Roles which are going to be notified by the plugin about significant actions to perform",
|
|
99
|
+
"page.settings.form.badWords.label": "Bad words filtering",
|
|
100
|
+
"page.settings.form.badWords.hint": "If enabled, every post / update of comment is going to be checked against bad wording",
|
|
101
|
+
"page.settings.form.gqlAuth.label": "GraphQL queries authorization",
|
|
102
|
+
"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": "Enable approval flow",
|
|
104
|
+
"page.settings.form.approvalFlow.hint": "Comments for \"{name}\" are going to be taken through manual approval flow",
|
|
105
|
+
"page.settings.form.entryLabel.label": "Title fields",
|
|
106
|
+
"page.settings.form.entryLabel.placeholder": "Select at least one or leave empty to apply defaults",
|
|
107
|
+
"page.settings.form.entryLabel.hint": "If left empty title rendering is going to take following ordered fields: \"Title\", \"Subject\" & \"Name\"",
|
|
108
|
+
"page.settings.actions.restore.confirmation.header": "Restore default configuration",
|
|
109
|
+
"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
|
+
"page.settings.actions.restore.confirmation.button.confirm": "Yes, I want to restore",
|
|
111
|
+
"page.settings.notification.submit.success": "Configuration has been saved successfully",
|
|
112
|
+
"page.settings.notification.submit.error": "Failed to save configuration. Try again.",
|
|
113
|
+
"page.settings.notification.restore.success": "Configuration has been restored successfully",
|
|
114
|
+
"page.settings.notification.restore.error": "Failed to restore configuration. Try again.",
|
|
115
|
+
"page.settings.loading": "Fetching configuration...",
|
|
82
116
|
"compontents.confirmation.dialog.header": "Confirmation",
|
|
83
117
|
"compontents.confirmation.dialog.description": "Do you really want to perform this action?",
|
|
84
118
|
"compontents.confirmation.dialog.button.confirm": "Yes, I do",
|
|
85
|
-
"compontents.confirmation.dialog.button.cancel": "Cancel"
|
|
119
|
+
"compontents.confirmation.dialog.button.cancel": "Cancel",
|
|
120
|
+
"compontents.toogle.enabled": "Enabled",
|
|
121
|
+
"compontents.toogle.disabled": "Disabled"
|
|
86
122
|
}
|