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.
Files changed (50) hide show
  1. package/README.md +55 -8
  2. package/__mocks__/pages.settings.json +25 -0
  3. package/__mocks__/strapi.js +207 -0
  4. package/admin/src/components/ConfirmationDialog/index.js +56 -0
  5. package/admin/src/components/Item/ItemCardBadge/index.js +2 -1
  6. package/admin/src/components/Item/ItemCardHeader/index.js +8 -13
  7. package/admin/src/components/Item/index.js +10 -13
  8. package/admin/src/components/RestartAlert/index.js +8 -0
  9. package/admin/src/components/Search/index.js +21 -23
  10. package/admin/src/hooks/useAllContentTypes.js +13 -0
  11. package/admin/src/hooks/useNavigationConfig.js +58 -0
  12. package/admin/src/index.js +24 -1
  13. package/admin/src/pages/SettingsPage/index.js +311 -0
  14. package/admin/src/pages/View/components/NavigationHeader/index.js +39 -23
  15. package/admin/src/pages/View/components/NavigationItemForm/index.js +49 -9
  16. package/admin/src/pages/View/components/NavigationItemPopup/NavigationItemPopupFooter.js +3 -6
  17. package/admin/src/pages/View/components/NavigationItemPopup/NavigationItemPopupHeader.js +3 -7
  18. package/admin/src/pages/View/components/NavigationItemPopup/index.js +3 -5
  19. package/admin/src/pages/View/index.js +29 -20
  20. package/admin/src/pages/View/utils/parsers.js +7 -3
  21. package/admin/src/translations/en.json +52 -10
  22. package/admin/src/translations/fr.json +4 -4
  23. package/admin/src/utils/api.js +51 -0
  24. package/admin/src/utils/index.js +20 -0
  25. package/package.json +7 -6
  26. package/server/bootstrap.js +30 -1
  27. package/server/content-types/navigation/schema.json +45 -0
  28. package/server/content-types/navigation-item/schema.json +1 -1
  29. package/server/controllers/navigation.js +30 -5
  30. package/server/graphql/index.js +3 -4
  31. package/server/graphql/queries/render-navigation.js +4 -3
  32. package/server/graphql/types/content-types-name-fields.js +4 -2
  33. package/server/graphql/types/navigation-related.js +2 -2
  34. package/server/routes/admin.js +24 -1
  35. package/server/services/__tests__/functions.test.js +48 -0
  36. package/server/services/__tests__/navigation.test.js +84 -77
  37. package/server/services/navigation.js +58 -18
  38. package/server/services/utils/functions.js +45 -12
  39. package/strapi-server.js +0 -2
  40. package/yarn-error.log +5263 -0
  41. package/.circleci/config.yml +0 -48
  42. package/.eslintrc +0 -35
  43. package/.github/pull_request_template.md +0 -13
  44. package/.github/stale.yml +0 -15
  45. package/.nvmrc +0 -1
  46. package/codecov.yml +0 -3
  47. package/public/assets/logo.png +0 -0
  48. package/public/assets/preview.png +0 -0
  49. package/server/content-types/navigation/schema.js +0 -45
  50. 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;
@@ -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
- structureHAsChanged,
17
+ structureHasChanged,
18
+ handleChangeSelection,
15
19
  handleSave,
16
20
  }) => {
17
21
  const { formatMessage } = useIntl();
18
-
19
22
  return (
20
23
  <HeaderLayout
21
- primaryAction={
24
+ primaryAction={
22
25
  <Stack horizontal size={2}>
23
- <Button
24
- onClick={handleSave}
25
- startIcon={<Check />}
26
- disabled={structureHasErrors || !structureHAsChanged}
27
- type="submit"
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
- {formatMessage(getTrad('submit.cta.save'))}
30
- </Button>
31
- {/* <MoreButton
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
- </Stack>
37
- }
38
- title={formatMessage({
39
- id: getTrad('header.title'),
40
- defaultMessage: 'UI Navigation',
41
- })}
42
- subtitle={formatMessage({
43
- id: getTrad('header.description'),
44
- defaultMessage: 'Define your portal navigation',
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
- {formatMessage(getTrad('popup.item.form.button.cancel'))}
14
+ {getMessage('popup.item.form.button.cancel')}
18
15
  </Button>
19
16
  }
20
17
  endActions={
21
18
  <Button onClick={handleSubmit} disabled={submitDisabled}>
22
- {formatMessage(getTrad(`popup.item.form.button.save`))}
19
+ {getMessage(`popup.item.form.button.save`)}
23
20
  </Button>
24
21
  }
25
22
  />