strapi-plugin-navigation 2.0.0-rc.1 → 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/README.md +55 -8
- package/__mocks__/pages.settings.json +25 -0
- package/__mocks__/strapi.js +207 -0
- package/admin/src/components/ConfirmationDialog/index.js +56 -0
- package/admin/src/components/Item/ItemCardBadge/index.js +2 -1
- package/admin/src/components/Item/ItemCardHeader/index.js +8 -13
- package/admin/src/components/Item/index.js +10 -13
- package/admin/src/components/RestartAlert/index.js +8 -0
- package/admin/src/components/Search/index.js +21 -23
- package/admin/src/hooks/useAllContentTypes.js +13 -0
- package/admin/src/hooks/useNavigationConfig.js +58 -0
- package/admin/src/index.js +24 -1
- package/admin/src/pages/SettingsPage/index.js +311 -0
- package/admin/src/pages/View/components/NavigationHeader/index.js +39 -23
- package/admin/src/pages/View/components/NavigationItemForm/index.js +49 -9
- package/admin/src/pages/View/components/NavigationItemPopup/NavigationItemPopupFooter.js +3 -6
- package/admin/src/pages/View/components/NavigationItemPopup/NavigationItemPopupHeader.js +3 -7
- package/admin/src/pages/View/components/NavigationItemPopup/index.js +3 -5
- package/admin/src/pages/View/index.js +29 -20
- package/admin/src/pages/View/utils/parsers.js +7 -3
- package/admin/src/translations/en.json +52 -10
- package/admin/src/translations/fr.json +4 -4
- package/admin/src/utils/api.js +51 -0
- package/admin/src/utils/index.js +20 -0
- package/package.json +7 -6
- package/server/bootstrap.js +30 -1
- package/server/content-types/navigation/schema.json +45 -0
- package/server/content-types/navigation-item/schema.json +1 -1
- package/server/controllers/navigation.js +30 -5
- package/server/graphql/index.js +3 -4
- package/server/graphql/queries/render-navigation.js +4 -3
- package/server/graphql/types/content-types-name-fields.js +4 -2
- package/server/graphql/types/navigation-related.js +2 -2
- package/server/routes/admin.js +24 -1
- package/server/services/__tests__/functions.test.js +48 -0
- package/server/services/__tests__/navigation.test.js +84 -77
- package/server/services/navigation.js +58 -18
- package/server/services/utils/functions.js +45 -12
- package/strapi-server.js +0 -2
- package/yarn-error.log +5263 -0
- package/.circleci/config.yml +0 -48
- package/.eslintrc +0 -35
- package/.github/pull_request_template.md +0 -13
- package/.github/stale.yml +0 -15
- package/.nvmrc +0 -1
- package/codecov.yml +0 -3
- package/public/assets/logo.png +0 -0
- package/public/assets/preview.png +0 -0
- package/server/content-types/navigation/schema.js +0 -45
- package/server/register.js +0 -5
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { useQuery, useQueryClient } from 'react-query';
|
|
2
|
+
import { useNotification } from '@strapi/helper-plugin';
|
|
3
|
+
import { fetchNavigationConfig, restartStrapi, restoreNavigationConfig, updateNavigationConfig } from '../utils/api';
|
|
4
|
+
import { getTrad } from '../translations';
|
|
5
|
+
|
|
6
|
+
const useNavigationConfig = () => {
|
|
7
|
+
const queryClient = useQueryClient();
|
|
8
|
+
const toggleNotification = useNotification();
|
|
9
|
+
const { isLoading, data, err } = useQuery('navigationConfig', () =>
|
|
10
|
+
fetchNavigationConfig(toggleNotification)
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
const handleError = (type) => {
|
|
14
|
+
toggleNotification({
|
|
15
|
+
type: 'warning',
|
|
16
|
+
message: getTrad(`pages.settings.notification.${type}.error`),
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const handleSuccess = async (type) => {
|
|
21
|
+
await queryClient.invalidateQueries('navigationConfig');
|
|
22
|
+
toggleNotification({
|
|
23
|
+
type: 'success',
|
|
24
|
+
message: getTrad(`pages.settings.notification.${type}.success`),
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const submitMutation = async (...args) => {
|
|
29
|
+
try {
|
|
30
|
+
await updateNavigationConfig(...args);
|
|
31
|
+
await handleSuccess('submit');
|
|
32
|
+
} catch (e) {
|
|
33
|
+
handleError('submit');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const restoreMutation = async (...args) => {
|
|
38
|
+
try {
|
|
39
|
+
await restoreNavigationConfig(...args);
|
|
40
|
+
await handleSuccess('restore');
|
|
41
|
+
} catch (e) {
|
|
42
|
+
handleError('restore');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const restartMutation = async (...args) => {
|
|
47
|
+
try {
|
|
48
|
+
await restartStrapi(...args);
|
|
49
|
+
await handleSuccess('restart');
|
|
50
|
+
} catch (e) {
|
|
51
|
+
handleError('restart');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { data, isLoading, err, submitMutation, restoreMutation, restartMutation };
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export default useNavigationConfig;
|
package/admin/src/index.js
CHANGED
|
@@ -3,11 +3,34 @@ import pluginPkg from '../../package.json';
|
|
|
3
3
|
import pluginId from './pluginId';
|
|
4
4
|
import pluginPermissions from './permissions';
|
|
5
5
|
import NavigationIcon from './components/icons/navigation';
|
|
6
|
-
|
|
6
|
+
import { getTrad } from './translations';
|
|
7
7
|
const name = pluginPkg.strapi.name;
|
|
8
8
|
|
|
9
9
|
export default {
|
|
10
10
|
register(app) {
|
|
11
|
+
app.createSettingSection(
|
|
12
|
+
{
|
|
13
|
+
id: pluginId,
|
|
14
|
+
intlLabel: { id: getTrad('pages.settings.section.title'), defaultMessage: 'Navigation Plugin' },
|
|
15
|
+
},
|
|
16
|
+
[
|
|
17
|
+
{
|
|
18
|
+
intlLabel: {
|
|
19
|
+
id: getTrad('pages.settings.section.subtitle'),
|
|
20
|
+
defaultMessage: 'Configuration',
|
|
21
|
+
},
|
|
22
|
+
id: 'navigation',
|
|
23
|
+
to: `/settings/${pluginId}`,
|
|
24
|
+
Component: async () => {
|
|
25
|
+
const component = await import(
|
|
26
|
+
/* webpackChunkName: "navigation-settings" */ './pages/SettingsPage'
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
return component;
|
|
30
|
+
},
|
|
31
|
+
permissions: pluginPermissions.access,
|
|
32
|
+
}
|
|
33
|
+
]);
|
|
11
34
|
app.addMenuLink({
|
|
12
35
|
to: `/plugins/${pluginId}`,
|
|
13
36
|
icon: NavigationIcon,
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Formik } from 'formik';
|
|
3
|
+
import { isEmpty, capitalize, isEqual } from 'lodash';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
CheckPermissions,
|
|
7
|
+
LoadingIndicatorPage,
|
|
8
|
+
Form,
|
|
9
|
+
useOverlayBlocker,
|
|
10
|
+
useAutoReloadOverlayBlocker,
|
|
11
|
+
} from '@strapi/helper-plugin';
|
|
12
|
+
import { Main } from '@strapi/design-system/Main';
|
|
13
|
+
import { ContentLayout, HeaderLayout } from '@strapi/design-system/Layout';
|
|
14
|
+
import { Button } from '@strapi/design-system/Button';
|
|
15
|
+
import { Box } from '@strapi/design-system/Box';
|
|
16
|
+
import { Stack } from '@strapi/design-system/Stack';
|
|
17
|
+
import { Typography } from '@strapi/design-system/Typography';
|
|
18
|
+
import { Grid, GridItem } from '@strapi/design-system/Grid';
|
|
19
|
+
import { ToggleInput } from '@strapi/design-system/ToggleInput';
|
|
20
|
+
import { NumberInput } from '@strapi/design-system/NumberInput';
|
|
21
|
+
import { Select, Option } from '@strapi/design-system/Select';
|
|
22
|
+
import { Check, Refresh, Play } from '@strapi/icons';
|
|
23
|
+
import { SettingsPageTitle } from '@strapi/helper-plugin';
|
|
24
|
+
import {
|
|
25
|
+
Card,
|
|
26
|
+
CardBody,
|
|
27
|
+
CardContent,
|
|
28
|
+
} from '@strapi/design-system/Card';
|
|
29
|
+
|
|
30
|
+
import permissions from '../../permissions';
|
|
31
|
+
import useNavigationConfig from '../../hooks/useNavigationConfig';
|
|
32
|
+
import useAllContentTypes from '../../hooks/useAllContentTypes';
|
|
33
|
+
import { navigationItemAdditionalFields } from '../View/utils/enums';
|
|
34
|
+
import ConfirmationDialog from '../../components/ConfirmationDialog';
|
|
35
|
+
import RestartAlert from '../../components/RestartAlert';
|
|
36
|
+
import { getMessage } from '../../utils';
|
|
37
|
+
|
|
38
|
+
const SettingsPage = () => {
|
|
39
|
+
const { lockApp, unlockApp } = useOverlayBlocker();
|
|
40
|
+
const { lockAppWithAutoreload, unlockAppWithAutoreload } = useAutoReloadOverlayBlocker();
|
|
41
|
+
const [isRestorePopupOpen, setIsRestorePopupOpen] = useState(false);
|
|
42
|
+
const [isRestartRequired, setIsRestartRequired] = useState(false);
|
|
43
|
+
const { data: navigationConfigData, isLoading: isConfigLoading, err: configErr, submitMutation, restoreMutation, restartMutation } = useNavigationConfig();
|
|
44
|
+
const { data: allContentTypesData, isLoading: isContentTypesLoading, err: contentTypesErr } = useAllContentTypes();
|
|
45
|
+
const isLoading = isConfigLoading || isContentTypesLoading;
|
|
46
|
+
const isError = configErr || contentTypesErr;
|
|
47
|
+
|
|
48
|
+
const preparePayload = ({ selectedContentTypes, nameFields, audienceFieldChecked, allowedLevels }) => ({
|
|
49
|
+
contentTypes: selectedContentTypes,
|
|
50
|
+
contentTypesNameFields: nameFields,
|
|
51
|
+
additionalFields: audienceFieldChecked ? [navigationItemAdditionalFields.AUDIENCE] : [],
|
|
52
|
+
allowedLevels: allowedLevels,
|
|
53
|
+
gql: {
|
|
54
|
+
navigationItemRelated: selectedContentTypes.map(uid => allContentTypes.find(ct => ct.uid === uid).info.displayName)
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
const onSave = async (form) => {
|
|
58
|
+
lockApp();
|
|
59
|
+
const payload = preparePayload(form);
|
|
60
|
+
await submitMutation({ body: payload });
|
|
61
|
+
const isContentTypesChanged = !isEqual(payload.contentTypes, navigationConfigData.contentTypes);
|
|
62
|
+
if (isContentTypesChanged && navigationConfigData.isGQLPluginEnabled) {
|
|
63
|
+
setIsRestartRequired(true);
|
|
64
|
+
}
|
|
65
|
+
unlockApp();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const onPopupClose = async (isConfirmed) => {
|
|
69
|
+
setIsRestorePopupOpen(false);
|
|
70
|
+
if (isConfirmed) {
|
|
71
|
+
lockApp();
|
|
72
|
+
await restoreMutation();
|
|
73
|
+
unlockApp();
|
|
74
|
+
setIsRestartRequired(true);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const handleRestart = async () => {
|
|
79
|
+
lockAppWithAutoreload();
|
|
80
|
+
await restartMutation();
|
|
81
|
+
setIsRestartRequired(false);
|
|
82
|
+
unlockAppWithAutoreload();
|
|
83
|
+
};
|
|
84
|
+
const handleRestartDiscard = () => setIsRestartRequired(false);
|
|
85
|
+
|
|
86
|
+
const prepareNameFieldFor = (uid, current, value) => ({
|
|
87
|
+
...current,
|
|
88
|
+
[uid]: value && !isEmpty(value) ? [...value] : undefined,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
if (isLoading || isError) {
|
|
92
|
+
return (
|
|
93
|
+
<>
|
|
94
|
+
<SettingsPageTitle
|
|
95
|
+
name={getMessage('Settings.email.plugin.title', 'Configuration')}
|
|
96
|
+
/>
|
|
97
|
+
<LoadingIndicatorPage>
|
|
98
|
+
Fetching plugin config...
|
|
99
|
+
</LoadingIndicatorPage>
|
|
100
|
+
</>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const allContentTypes = !isLoading && Object.values(allContentTypesData).filter(item => item.uid.includes('api::'));
|
|
105
|
+
const selectedContentTypes = navigationConfigData?.contentTypes.map(item => item.uid);
|
|
106
|
+
const audienceFieldChecked = navigationConfigData?.additionalFields.includes(navigationItemAdditionalFields.AUDIENCE);
|
|
107
|
+
const allowedLevels = navigationConfigData?.allowedLevels;
|
|
108
|
+
const nameFields = navigationConfigData?.contentTypesNameFields
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<>
|
|
112
|
+
<SettingsPageTitle
|
|
113
|
+
name={getMessage('Settings.email.plugin.title', 'Configuration')}
|
|
114
|
+
/>
|
|
115
|
+
<Main labelledBy="title">
|
|
116
|
+
<Formik
|
|
117
|
+
initialValues={{
|
|
118
|
+
selectedContentTypes,
|
|
119
|
+
audienceFieldChecked,
|
|
120
|
+
allowedLevels,
|
|
121
|
+
nameFields,
|
|
122
|
+
}}
|
|
123
|
+
onSubmit={onSave}
|
|
124
|
+
>
|
|
125
|
+
{({ handleSubmit, setFieldValue, values }) => (
|
|
126
|
+
<Form noValidate onSubmit={handleSubmit}>
|
|
127
|
+
<HeaderLayout
|
|
128
|
+
title={getMessage('pages.settings.header.title')}
|
|
129
|
+
subtitle={getMessage('pages.settings.header.description')}
|
|
130
|
+
primaryAction={
|
|
131
|
+
<CheckPermissions permissions={permissions.access}>
|
|
132
|
+
<Button type="submit" startIcon={<Check />} disabled={isRestartRequired}>
|
|
133
|
+
{getMessage('pages.settings.actions.submit')}
|
|
134
|
+
</Button>
|
|
135
|
+
</CheckPermissions>
|
|
136
|
+
}
|
|
137
|
+
/>
|
|
138
|
+
<ContentLayout>
|
|
139
|
+
<Stack size={7}>
|
|
140
|
+
{isRestartRequired && (
|
|
141
|
+
<RestartAlert
|
|
142
|
+
closeLabel={getMessage('pages.settings.actions.restart.alert.cancel')}
|
|
143
|
+
title={getMessage('pages.settings.actions.restart.alert.title')}
|
|
144
|
+
action={<Box><Button onClick={handleRestart} startIcon={<Play />}>{getMessage('pages.settings.actions.restart')}</Button></Box>}
|
|
145
|
+
onClose={handleRestartDiscard}>
|
|
146
|
+
{getMessage('pages.settings.actions.restart.alert.description')}
|
|
147
|
+
</RestartAlert>)}
|
|
148
|
+
<Box
|
|
149
|
+
background="neutral0"
|
|
150
|
+
hasRadius
|
|
151
|
+
shadow="filterShadow"
|
|
152
|
+
padding={6}
|
|
153
|
+
>
|
|
154
|
+
<Stack size={4}>
|
|
155
|
+
<Typography variant="delta" as="h2">
|
|
156
|
+
{getMessage('pages.settings.general.title')}
|
|
157
|
+
</Typography>
|
|
158
|
+
<Grid gap={4}>
|
|
159
|
+
<GridItem col={12} s={12} xs={12}>
|
|
160
|
+
<Select
|
|
161
|
+
name="selectedContentTypes"
|
|
162
|
+
label={getMessage('pages.settings.form.contentTypes.label')}
|
|
163
|
+
placeholder={getMessage('pages.settings.form.contentTypes.placeholder')}
|
|
164
|
+
hint={getMessage('pages.settings.form.contentTypes.hint')}
|
|
165
|
+
onClear={() => setFieldValue('selectedContentTypes', [], false)}
|
|
166
|
+
value={values.selectedContentTypes}
|
|
167
|
+
onChange={(value) => setFieldValue('selectedContentTypes', value, false)}
|
|
168
|
+
multi
|
|
169
|
+
withTags
|
|
170
|
+
disabled={isRestartRequired}
|
|
171
|
+
>
|
|
172
|
+
{allContentTypes.map((item) => <Option key={item.uid} value={item.uid}>{item.info.displayName}</Option>)}
|
|
173
|
+
</Select>
|
|
174
|
+
</GridItem>
|
|
175
|
+
<GridItem col={3} s={6} xs={12}>
|
|
176
|
+
<NumberInput
|
|
177
|
+
name="allowedLevels"
|
|
178
|
+
label={getMessage('pages.settings.form.allowedLevels.label')}
|
|
179
|
+
placeholder={getMessage('pages.settings.form.allowedLevels.placeholder')}
|
|
180
|
+
hint={getMessage('pages.settings.form.allowedLevels.hint')}
|
|
181
|
+
onValueChange={(value) => setFieldValue('allowedLevels', value, false)}
|
|
182
|
+
value={values.allowedLevels}
|
|
183
|
+
disabled={isRestartRequired}
|
|
184
|
+
/>
|
|
185
|
+
</GridItem>
|
|
186
|
+
</Grid>
|
|
187
|
+
</Stack>
|
|
188
|
+
</Box>
|
|
189
|
+
<Box
|
|
190
|
+
background="neutral0"
|
|
191
|
+
hasRadius
|
|
192
|
+
shadow="filterShadow"
|
|
193
|
+
padding={6}
|
|
194
|
+
>
|
|
195
|
+
<Stack size={4}>
|
|
196
|
+
<Typography variant="delta" as="h2">
|
|
197
|
+
{getMessage('pages.settings.additional.title')}
|
|
198
|
+
</Typography>
|
|
199
|
+
<Grid gap={4}>
|
|
200
|
+
<GridItem col={6} s={12} xs={12}>
|
|
201
|
+
<ToggleInput
|
|
202
|
+
name="audienceFieldChecked"
|
|
203
|
+
label={getMessage('pages.settings.form.audience.label')}
|
|
204
|
+
hint={getMessage('pages.settings.form.audience.hint')}
|
|
205
|
+
checked={values.audienceFieldChecked}
|
|
206
|
+
onChange={({ target: { checked } }) => setFieldValue('audienceFieldChecked', checked, false)}
|
|
207
|
+
onLabel="Enabled"
|
|
208
|
+
offLabel="Disabled"
|
|
209
|
+
disabled={isRestartRequired}
|
|
210
|
+
/>
|
|
211
|
+
</GridItem>
|
|
212
|
+
</Grid>
|
|
213
|
+
</Stack>
|
|
214
|
+
</Box>
|
|
215
|
+
{!isEmpty(values.selectedContentTypes) && (
|
|
216
|
+
<Box
|
|
217
|
+
background="neutral0"
|
|
218
|
+
hasRadius
|
|
219
|
+
shadow="filterShadow"
|
|
220
|
+
padding={6}
|
|
221
|
+
>
|
|
222
|
+
<Stack size={4}>
|
|
223
|
+
<Typography variant="delta" as="h2">
|
|
224
|
+
{getMessage('pages.settings.nameField.title')}
|
|
225
|
+
</Typography>
|
|
226
|
+
<Grid gap={4}>
|
|
227
|
+
{values.selectedContentTypes.map(uid => {
|
|
228
|
+
const { attributes, info: { displayName } } = allContentTypes.find(item => item.uid == uid);
|
|
229
|
+
const stringAttributes = Object.keys(attributes).filter(_ => attributes[_].type === 'string');
|
|
230
|
+
|
|
231
|
+
return !isEmpty(stringAttributes) && (
|
|
232
|
+
<GridItem key={`collectionSettings-${uid}`} col={6} s={12} xs={12}>
|
|
233
|
+
<Card background="primary100" borderColor="primary200">
|
|
234
|
+
<CardBody>
|
|
235
|
+
<CardContent style={{ width: '100%' }}>
|
|
236
|
+
<Stack size={4}>
|
|
237
|
+
<Typography variant="epsilon" fontWeight="semibold" as="h3">{displayName}</Typography>
|
|
238
|
+
<Select
|
|
239
|
+
name={`collectionSettings-${uid}-entryLabel`}
|
|
240
|
+
label={getMessage('pages.settings.form.nameField.label')}
|
|
241
|
+
hint={getMessage('pages.settings.form.nameField.hint')}
|
|
242
|
+
placeholder={getMessage('pages.settings.form.nameField.placeholder')}
|
|
243
|
+
onClear={() => null}
|
|
244
|
+
value={values.nameFields[uid] || []}
|
|
245
|
+
onChange={(value) => setFieldValue('nameFields', prepareNameFieldFor(uid, values.nameFields, value))}
|
|
246
|
+
multi
|
|
247
|
+
withTags
|
|
248
|
+
disabled={isRestartRequired}
|
|
249
|
+
>
|
|
250
|
+
{stringAttributes.map(key =>
|
|
251
|
+
(<Option key={uid + key} value={key}>{capitalize(key.split('_').join(' '))}</Option>))}
|
|
252
|
+
</Select>
|
|
253
|
+
</Stack>
|
|
254
|
+
</CardContent>
|
|
255
|
+
</CardBody>
|
|
256
|
+
</Card>
|
|
257
|
+
</GridItem>
|
|
258
|
+
);
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
</Grid>
|
|
262
|
+
</Stack>
|
|
263
|
+
</Box>
|
|
264
|
+
)}
|
|
265
|
+
<Box
|
|
266
|
+
background="neutral0"
|
|
267
|
+
hasRadius
|
|
268
|
+
shadow="filterShadow"
|
|
269
|
+
padding={6}
|
|
270
|
+
>
|
|
271
|
+
<Stack size={4}>
|
|
272
|
+
<Typography variant="delta" as="h2">
|
|
273
|
+
{getMessage('pages.settings.restoring.title')}
|
|
274
|
+
</Typography>
|
|
275
|
+
<Grid gap={4}>
|
|
276
|
+
<GridItem col={12} s={12} xs={12}>
|
|
277
|
+
<Typography>
|
|
278
|
+
{getMessage('pages.settings.actions.restore.description')}
|
|
279
|
+
</Typography>
|
|
280
|
+
</GridItem>
|
|
281
|
+
<GridItem col={6} s={12} xs={12}>
|
|
282
|
+
<CheckPermissions permissions={permissions.access}>
|
|
283
|
+
<Button variant="danger-light" startIcon={<Refresh />} onClick={() => setIsRestorePopupOpen(true)}>
|
|
284
|
+
{getMessage('pages.settings.actions.restore')}
|
|
285
|
+
</Button>
|
|
286
|
+
</CheckPermissions>
|
|
287
|
+
<ConfirmationDialog
|
|
288
|
+
isVisible={isRestorePopupOpen}
|
|
289
|
+
header={getMessage('pages.settings.actions.restore.confirmation.header')}
|
|
290
|
+
labelConfirm={getMessage('pages.settings.actions.restore.confirmation.confirm')}
|
|
291
|
+
iconConfirm={<Refresh />}
|
|
292
|
+
onConfirm={() => onPopupClose(true)}
|
|
293
|
+
onCancel={() => onPopupClose(false)}>
|
|
294
|
+
{getMessage('pages.settings.actions.restore.confirmation.description')}
|
|
295
|
+
</ConfirmationDialog>
|
|
296
|
+
</GridItem>
|
|
297
|
+
</Grid>
|
|
298
|
+
</Stack>
|
|
299
|
+
</Box>
|
|
300
|
+
</Stack>
|
|
301
|
+
</ContentLayout>
|
|
302
|
+
</Form>
|
|
303
|
+
)}
|
|
304
|
+
</Formik>
|
|
305
|
+
</Main>
|
|
306
|
+
</>
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
export default SettingsPage;
|
|
@@ -7,43 +7,59 @@ import Check from '@strapi/icons/Check';
|
|
|
7
7
|
import More from '@strapi/icons/More';
|
|
8
8
|
import { getTrad } from '../../../../translations';
|
|
9
9
|
import { MoreButton } from './styles';
|
|
10
|
-
|
|
10
|
+
import { Select, Option } from '@strapi/design-system/Select';
|
|
11
|
+
import { Box } from '@strapi/design-system/Box'
|
|
11
12
|
|
|
12
13
|
const NavigationHeader = ({
|
|
14
|
+
activeNavigation,
|
|
15
|
+
availableNavigations,
|
|
13
16
|
structureHasErrors,
|
|
14
|
-
|
|
17
|
+
structureHasChanged,
|
|
18
|
+
handleChangeSelection,
|
|
15
19
|
handleSave,
|
|
16
20
|
}) => {
|
|
17
21
|
const { formatMessage } = useIntl();
|
|
18
|
-
|
|
19
22
|
return (
|
|
20
23
|
<HeaderLayout
|
|
21
|
-
|
|
24
|
+
primaryAction={
|
|
22
25
|
<Stack horizontal size={2}>
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
<Box width="10vw">
|
|
27
|
+
<Select
|
|
28
|
+
type="select"
|
|
29
|
+
placeholder={'Change navigation'}
|
|
30
|
+
name={`navigationSelect`}
|
|
31
|
+
onChange={handleChangeSelection}
|
|
32
|
+
value={activeNavigation?.id}
|
|
33
|
+
size="S"
|
|
34
|
+
style={null}
|
|
28
35
|
>
|
|
29
|
-
{
|
|
30
|
-
</
|
|
31
|
-
|
|
36
|
+
{availableNavigations.map(({ id, name }) => <Option key={id} value={id}>{name}</Option>)}
|
|
37
|
+
</Select >
|
|
38
|
+
</Box>
|
|
39
|
+
<Button
|
|
40
|
+
onClick={handleSave}
|
|
41
|
+
startIcon={<Check />}
|
|
42
|
+
disabled={structureHasErrors || !structureHasChanged}
|
|
43
|
+
type="submit"
|
|
44
|
+
>
|
|
45
|
+
{formatMessage(getTrad('submit.cta.save'))}
|
|
46
|
+
</Button>
|
|
47
|
+
{/* <MoreButton
|
|
32
48
|
id="more"
|
|
33
49
|
label="More"
|
|
34
50
|
icon={<More />}
|
|
35
51
|
/> */}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
52
|
+
</Stack>
|
|
53
|
+
}
|
|
54
|
+
title={formatMessage({
|
|
55
|
+
id: getTrad('header.title'),
|
|
56
|
+
defaultMessage: 'UI Navigation',
|
|
57
|
+
})}
|
|
58
|
+
subtitle={formatMessage({
|
|
59
|
+
id: getTrad('header.description'),
|
|
60
|
+
defaultMessage: 'Define your portal navigation',
|
|
61
|
+
})}
|
|
62
|
+
/>
|
|
47
63
|
);
|
|
48
64
|
};
|
|
49
65
|
|
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
import React, { useEffect, useMemo, useState, useCallback } from 'react';
|
|
2
|
-
import { useIntl } from 'react-intl';
|
|
3
2
|
import { debounce, find, get, isEmpty, isEqual, isNil, isString } from 'lodash';
|
|
4
3
|
import PropTypes from 'prop-types';
|
|
5
4
|
import { Formik } from 'formik'
|
|
6
5
|
|
|
7
6
|
// Design System
|
|
8
7
|
import { ModalBody } from '@strapi/design-system/ModalLayout';
|
|
8
|
+
import { Select, Option } from '@strapi/design-system/Select';
|
|
9
9
|
import { Grid, GridItem } from '@strapi/design-system/Grid';
|
|
10
10
|
import { Form, GenericInput } from '@strapi/helper-plugin';
|
|
11
11
|
|
|
12
12
|
import { NavigationItemPopupFooter } from '../NavigationItemPopup/NavigationItemPopupFooter';
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
import { navigationItemType } from '../../utils/enums';
|
|
15
|
+
import { navigationItemAdditionalFields, navigationItemType } from '../../utils/enums';
|
|
16
16
|
import slugify from 'slugify';
|
|
17
17
|
import { extractRelatedItemLabel } from '../../utils/parsers';
|
|
18
18
|
import { form as formDefinition } from './utils/form';
|
|
19
19
|
import { checkFormValidity } from '../../utils/form';
|
|
20
20
|
import { getTradId } from '../../../../translations';
|
|
21
|
+
import { getMessage } from '../../../../utils';
|
|
21
22
|
|
|
22
23
|
const NavigationItemForm = ({
|
|
23
24
|
isLoading,
|
|
@@ -42,7 +43,6 @@ const NavigationItemForm = ({
|
|
|
42
43
|
const [form, setFormState] = useState({});
|
|
43
44
|
const [formErrors, setFormErrorsState] = useState({});
|
|
44
45
|
const { relatedType } = form;
|
|
45
|
-
const { formatMessage } = useIntl();
|
|
46
46
|
|
|
47
47
|
const relatedFieldName = `${inputsPrefix}related`;
|
|
48
48
|
|
|
@@ -52,22 +52,29 @@ const NavigationItemForm = ({
|
|
|
52
52
|
...data,
|
|
53
53
|
type: data.type || navigationItemType.INTERNAL,
|
|
54
54
|
related: data.related?.value,
|
|
55
|
-
relatedType: data.relatedType?.value
|
|
55
|
+
relatedType: data.relatedType?.value,
|
|
56
|
+
audience: data.audience?.map(item => item.id),
|
|
56
57
|
});
|
|
57
58
|
}
|
|
58
59
|
|
|
60
|
+
const audience = get(form, `${inputsPrefix}audience`, []);
|
|
61
|
+
const audienceOptions = availableAudience.map((item) => ({
|
|
62
|
+
value: get(item, 'id', " "),
|
|
63
|
+
label: get(item, 'name', " "),
|
|
64
|
+
}));
|
|
65
|
+
|
|
59
66
|
const generatePreviewPath = () => {
|
|
60
67
|
if (!isExternal) {
|
|
61
68
|
const value = `${data.levelPath !== '/' ? `${data.levelPath}` : ''}/${form.path !== '/' ? form.path || '' : ''}`;
|
|
62
69
|
return {
|
|
63
70
|
id: getTradId('popup.item.form.type.external.description'),
|
|
64
71
|
defaultMessage: '',
|
|
65
|
-
values: { value
|
|
72
|
+
values: { value }
|
|
66
73
|
}
|
|
67
74
|
}
|
|
68
75
|
return null;
|
|
69
76
|
};
|
|
70
|
-
|
|
77
|
+
|
|
71
78
|
const sanitizePayload = (payload = {}) => {
|
|
72
79
|
const { onItemClick, onItemLevelAddClick, related, relatedType, menuAttached, ...purePayload } = payload;
|
|
73
80
|
const sanitizedType = purePayload.type || navigationItemType.INTERNAL;
|
|
@@ -103,6 +110,10 @@ const NavigationItemForm = ({
|
|
|
103
110
|
const onTypeChange = ({ target: { name, value } }) =>
|
|
104
111
|
onChange({ target: { name, value: value ? navigationItemType.INTERNAL : navigationItemType.EXTERNAL } });
|
|
105
112
|
|
|
113
|
+
const onAudienceChange = (value) => {
|
|
114
|
+
onChange({target: {name: `${inputsPrefix}audience`, value}});
|
|
115
|
+
}
|
|
116
|
+
|
|
106
117
|
const onChange = ({ target: { name, value } }) => {
|
|
107
118
|
setFormState(prevState => ({
|
|
108
119
|
...prevState,
|
|
@@ -139,7 +150,7 @@ const NavigationItemForm = ({
|
|
|
139
150
|
.filter((item) => {
|
|
140
151
|
const usedContentTypeEntitiesOfSameType = usedContentTypeEntities
|
|
141
152
|
.filter(uctItem => relatedTypeSelectValue === uctItem.__collectionUid);
|
|
142
|
-
return !find(usedContentTypeEntitiesOfSameType, uctItem => (item.id === uctItem.id && uctItem.id !== form.related));
|
|
153
|
+
return !find(usedContentTypeEntitiesOfSameType, uctItem => (item.id === uctItem.id && uctItem.id !== form.related));
|
|
143
154
|
})
|
|
144
155
|
.map((item) => {
|
|
145
156
|
const label = appendLabelPublicationStatus(
|
|
@@ -153,8 +164,8 @@ const NavigationItemForm = ({
|
|
|
153
164
|
key: get(item, 'id'),
|
|
154
165
|
metadatas: {
|
|
155
166
|
intlLabel: {
|
|
156
|
-
id: label ||
|
|
157
|
-
defaultMessage: label ||
|
|
167
|
+
id: label || `${item.__collectionUid} ${item.id}`,
|
|
168
|
+
defaultMessage: label || `${item.__collectionUid} ${item.id}`,
|
|
158
169
|
}
|
|
159
170
|
},
|
|
160
171
|
value: item.id,
|
|
@@ -259,6 +270,10 @@ const NavigationItemForm = ({
|
|
|
259
270
|
}}
|
|
260
271
|
name={`${inputsPrefix}title`}
|
|
261
272
|
placeholder={{
|
|
273
|
+
id: "e.g. Blog",
|
|
274
|
+
defaultMessage: 'e.g. Blog',
|
|
275
|
+
}}
|
|
276
|
+
description={{
|
|
262
277
|
id: getTradId('popup.item.form.title.placeholder'),
|
|
263
278
|
defaultMessage: 'e.g. Blog',
|
|
264
279
|
}}
|
|
@@ -331,6 +346,15 @@ const NavigationItemForm = ({
|
|
|
331
346
|
onChange={onChangeRelatedType}
|
|
332
347
|
options={relatedTypeSelectOptions}
|
|
333
348
|
value={relatedTypeSelectValue}
|
|
349
|
+
disabled={isLoading || isEmpty(relatedTypeSelectOptions)}
|
|
350
|
+
description={
|
|
351
|
+
!isLoading && isEmpty(relatedTypeSelectOptions)
|
|
352
|
+
? {
|
|
353
|
+
id: getTradId('popup.item.form.relatedType.empty'),
|
|
354
|
+
defaultMessage: 'There are no more content types',
|
|
355
|
+
}
|
|
356
|
+
: undefined
|
|
357
|
+
}
|
|
334
358
|
/>
|
|
335
359
|
</GridItem>
|
|
336
360
|
{relatedTypeSelectValue && !isSingleSelected && (
|
|
@@ -367,6 +391,22 @@ const NavigationItemForm = ({
|
|
|
367
391
|
)}
|
|
368
392
|
</>
|
|
369
393
|
)}
|
|
394
|
+
|
|
395
|
+
{additionalFields.includes(navigationItemAdditionalFields.AUDIENCE) && (
|
|
396
|
+
<GridItem key={`${inputsPrefix}audience`} col={6} lg={12}>
|
|
397
|
+
<Select
|
|
398
|
+
id={`${inputsPrefix}audience`}
|
|
399
|
+
placeholder={getMessage('popup.item.form.audience.placeholder')}
|
|
400
|
+
label={getMessage('popup.item.form.audience.label')}
|
|
401
|
+
onChange={onAudienceChange}
|
|
402
|
+
value={audience}
|
|
403
|
+
multi
|
|
404
|
+
withTags
|
|
405
|
+
>
|
|
406
|
+
{audienceOptions.map(({ value, label }) => <Option key={value} value={value}>{label}</Option>)}
|
|
407
|
+
</Select>
|
|
408
|
+
</GridItem>
|
|
409
|
+
)}
|
|
370
410
|
</Grid>
|
|
371
411
|
</ModalBody>
|
|
372
412
|
</Form>
|
|
@@ -1,25 +1,22 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
|
-
import { useIntl } from 'react-intl';
|
|
4
3
|
|
|
5
4
|
import { Button } from '@strapi/design-system/Button';
|
|
6
5
|
import { ModalFooter } from '@strapi/design-system/ModalLayout';
|
|
7
|
-
|
|
8
|
-
import { getTrad } from '../../../../translations';
|
|
6
|
+
import { getMessage } from '../../../../utils';
|
|
9
7
|
|
|
10
8
|
export const NavigationItemPopupFooter = ({ handleCancel, handleSubmit, submitDisabled, formViewId }) => {
|
|
11
|
-
const { formatMessage } = useIntl();
|
|
12
9
|
|
|
13
10
|
return (
|
|
14
11
|
<ModalFooter
|
|
15
12
|
startActions={
|
|
16
13
|
<Button onClick={handleCancel} variant="tertiary">
|
|
17
|
-
{
|
|
14
|
+
{getMessage('popup.item.form.button.cancel')}
|
|
18
15
|
</Button>
|
|
19
16
|
}
|
|
20
17
|
endActions={
|
|
21
18
|
<Button onClick={handleSubmit} disabled={submitDisabled}>
|
|
22
|
-
{
|
|
19
|
+
{getMessage(`popup.item.form.button.save`)}
|
|
23
20
|
</Button>
|
|
24
21
|
}
|
|
25
22
|
/>
|