strapi-plugin-navigation 2.0.0-beta.4 → 2.0.1

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 (38) hide show
  1. package/README.md +46 -19
  2. package/__mocks__/pages.settings.json +25 -0
  3. package/__mocks__/strapi.js +196 -0
  4. package/admin/src/components/EmptyView/index.js +2 -1
  5. package/admin/src/components/Item/ItemCardBadge/index.js +15 -1
  6. package/admin/src/components/Item/ItemCardHeader/index.js +8 -15
  7. package/admin/src/components/Item/ItemCardRemovedOverlay/index.js +12 -0
  8. package/admin/src/components/Item/index.js +66 -23
  9. package/admin/src/components/NavigationItemList/index.js +8 -0
  10. package/admin/src/components/Search/index.js +21 -23
  11. package/admin/src/components/icons/navigation.js +14 -0
  12. package/admin/src/index.js +4 -3
  13. package/admin/src/pages/View/components/NavigationHeader/index.js +41 -35
  14. package/admin/src/pages/View/components/NavigationHeader/styles.js +13 -0
  15. package/admin/src/pages/View/components/NavigationItemForm/index.js +50 -7
  16. package/admin/src/pages/View/components/NavigationItemPopup/NavigationItemPopupHeader.js +2 -4
  17. package/admin/src/pages/View/components/NavigationItemPopup/index.js +1 -1
  18. package/admin/src/pages/View/index.js +31 -17
  19. package/admin/src/pages/View/utils/parsers.js +10 -4
  20. package/admin/src/permissions.js +8 -0
  21. package/admin/src/translations/en.json +9 -6
  22. package/package.json +5 -3
  23. package/permissions.js +11 -0
  24. package/server/bootstrap.js +5 -4
  25. package/server/content-types/navigation/schema.json +45 -0
  26. package/server/content-types/navigation-item/schema.json +1 -1
  27. package/server/graphql/types/navigation-related.js +1 -1
  28. package/server/services/__tests__/navigation.test.js +63 -78
  29. package/server/services/navigation.js +5 -5
  30. package/server/services/utils/functions.js +5 -12
  31. package/.circleci/config.yml +0 -48
  32. package/.eslintrc +0 -35
  33. package/.github/pull_request_template.md +0 -13
  34. package/.github/stale.yml +0 -15
  35. package/.nvmrc +0 -1
  36. package/admin/src/components/PluginIcon/index.js +0 -6
  37. package/codecov.yml +0 -3
  38. package/server/content-types/navigation/schema.js +0 -45
@@ -1,7 +1,7 @@
1
1
  import React, { useRef, useState, useEffect } from 'react';
2
2
  import { useIntl } from 'react-intl';
3
3
  import { IconButton } from '@strapi/design-system/IconButton';
4
- import { Searchbar, SearchForm } from '@strapi/design-system/Searchbar';
4
+ import { Searchbar } from '@strapi/design-system/Searchbar';
5
5
  import SearchIcon from "@strapi/icons/Search";
6
6
  import { getTradId } from '../../translations';
7
7
 
@@ -11,32 +11,30 @@ const Search = ({ value, setValue }) => {
11
11
  const { formatMessage } = useIntl();
12
12
 
13
13
  useEffect(() => {
14
- if (isOpen) {
15
- setTimeout(() => {
16
- wrapperRef.current.querySelector('input').focus();
17
- }, 0);
18
- }
14
+ if (isOpen) {
15
+ setTimeout(() => {
16
+ wrapperRef.current.querySelector('input').focus();
17
+ }, 0);
18
+ }
19
19
  }, [isOpen]);
20
-
20
+
21
21
  if (isOpen) {
22
22
  return (
23
23
  <div ref={wrapperRef}>
24
- <SearchForm>
25
- <Searchbar
26
- name="searchbar"
27
- onClear={() => setValue('')}
28
- value={value}
29
- size="S"
30
- onChange={(e) => setValue(e.target.value)}
31
- clearLabel="Clearing the search"
32
- placeholder={formatMessage({
33
- id: getTradId('popup.item.form.audience.placeholder'),
34
- defaultMessage: 'Type to start searching...',
35
- })}
36
- >
37
- Search for navigation items
38
- </Searchbar>
39
- </SearchForm>
24
+ <Searchbar
25
+ name="searchbar"
26
+ onClear={() => { setValue(''); setIsOpen(false); }}
27
+ value={value}
28
+ size="S"
29
+ onChange={(e) => setValue(e.target.value)}
30
+ clearLabel="Clearing the search"
31
+ placeholder={formatMessage({
32
+ id: getTradId('popup.item.form.audience.placeholder'),
33
+ defaultMessage: 'Type to start searching...',
34
+ })}
35
+ >
36
+ Search for navigation items
37
+ </Searchbar>
40
38
  </div>
41
39
  );
42
40
  } else {
@@ -0,0 +1,14 @@
1
+
2
+ import React from 'react';
3
+
4
+ const initSize = 92;
5
+
6
+ const NavigationIcon = ({ width = 24, height = 24 }) =>
7
+ <svg viewBox={`0 0 ${width} ${height}`} xmlns="http://www.w3.org/2000/svg"><g style={ {transform: `scale(${width/initSize})` } }>
8
+ <path d="M78,23.5H14c-3.6,0-6.5-2.9-6.5-6.5s2.9-6.5,6.5-6.5h64c3.6,0,6.5,2.9,6.5,6.5S81.6,23.5,78,23.5z M84.5,46
9
+ c0-3.6-2.9-6.5-6.5-6.5H14c-3.6,0-6.5,2.9-6.5,6.5s2.9,6.5,6.5,6.5h64C81.6,52.5,84.5,49.6,84.5,46z M84.5,75c0-3.6-2.9-6.5-6.5-6.5
10
+ H14c-3.6,0-6.5,2.9-6.5,6.5s2.9,6.5,6.5,6.5h64C81.6,81.5,84.5,78.6,84.5,75z"/>
11
+ </g>
12
+ </svg>;
13
+
14
+ export default NavigationIcon;
@@ -1,7 +1,8 @@
1
1
  import { prefixPluginTranslations } from '@strapi/helper-plugin';
2
- import PluginIcon from './components/PluginIcon';
3
2
  import pluginPkg from '../../package.json';
4
3
  import pluginId from './pluginId';
4
+ import pluginPermissions from './permissions';
5
+ import NavigationIcon from './components/icons/navigation';
5
6
 
6
7
  const name = pluginPkg.strapi.name;
7
8
 
@@ -9,7 +10,7 @@ export default {
9
10
  register(app) {
10
11
  app.addMenuLink({
11
12
  to: `/plugins/${pluginId}`,
12
- icon: PluginIcon,
13
+ icon: NavigationIcon,
13
14
  intlLabel: {
14
15
  id: `${pluginId}.plugin.name`,
15
16
  defaultMessage: 'Navigation',
@@ -19,7 +20,7 @@ export default {
19
20
 
20
21
  return component;
21
22
  },
22
- permissions: [],
23
+ permissions: pluginPermissions.access,
23
24
  });
24
25
  app.registerPlugin({
25
26
  id: pluginId,
@@ -3,57 +3,63 @@ import { useIntl } from 'react-intl';
3
3
  import { HeaderLayout } from '@strapi/design-system/Layout';
4
4
  import { Stack } from '@strapi/design-system/Stack';
5
5
  import { Button } from '@strapi/design-system/Button';
6
- import { IconButton } from '@strapi/design-system/IconButton';
7
6
  import Check from '@strapi/icons/Check';
8
7
  import More from '@strapi/icons/More';
9
- import Plus from '@strapi/icons/Plus';
10
- import styled from 'styled-components';
11
8
  import { getTrad } from '../../../../translations';
12
- import { transformToRESTPayload } from '../../utils/parsers';
13
- const MoreButton = styled(IconButton)`
14
- margin: ${({ theme }) => `0 ${theme.spaces[2]}`};
15
- padding: ${({ theme }) => theme.spaces[2]};
16
-
17
- svg {
18
- width: ${18 / 16}rem;
19
- height: ${18 / 16}rem;
20
- }
21
- `;
9
+ import { MoreButton } from './styles';
10
+ import { Select, Option } from '@strapi/design-system/Select';
11
+ import { Box } from '@strapi/design-system/Box'
22
12
 
23
13
  const NavigationHeader = ({
14
+ activeNavigation,
15
+ availableNavigations,
24
16
  structureHasErrors,
17
+ structureHasChanged,
18
+ handleChangeSelection,
25
19
  handleSave,
26
20
  }) => {
27
21
  const { formatMessage } = useIntl();
28
-
29
22
  return (
30
23
  <HeaderLayout
31
- primaryAction={
24
+ primaryAction={
32
25
  <Stack horizontal size={2}>
33
- <Button
34
- onClick={handleSave}
35
- startIcon={<Check />}
36
- disabled={structureHasErrors}
37
- 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}
38
35
  >
39
- {formatMessage(getTrad('submit.cta.save'))}
40
- </Button>
41
- <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
42
48
  id="more"
43
49
  label="More"
44
50
  icon={<More />}
45
- />
46
- </Stack>
47
- }
48
- title={formatMessage({
49
- id: getTrad('header.title'),
50
- defaultMessage: 'UI Navigation',
51
- })}
52
- subtitle={formatMessage({
53
- id: getTrad('header.description'),
54
- defaultMessage: 'Define your portal navigation',
55
- })}
56
- />
51
+ /> */}
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
+ />
57
63
  );
58
64
  };
59
65
 
@@ -0,0 +1,13 @@
1
+ import styled from 'styled-components';
2
+ import { IconButton } from '@strapi/design-system/IconButton';
3
+
4
+ export const MoreButton = styled(IconButton)`
5
+ margin: ${({ theme }) => `0 ${theme.spaces[2]}`};
6
+ padding: ${({ theme }) => theme.spaces[2]};
7
+
8
+ svg {
9
+ width: ${18 / 16}rem;
10
+ height: ${18 / 16}rem;
11
+ }
12
+ `;
13
+
@@ -6,18 +6,19 @@ import { Formik } from 'formik'
6
6
 
7
7
  // Design System
8
8
  import { ModalBody } from '@strapi/design-system/ModalLayout';
9
+ import { Select, Option } from '@strapi/design-system/Select';
9
10
  import { Grid, GridItem } from '@strapi/design-system/Grid';
10
11
  import { Form, GenericInput } from '@strapi/helper-plugin';
11
12
 
12
13
  import { NavigationItemPopupFooter } from '../NavigationItemPopup/NavigationItemPopupFooter';
13
14
 
14
15
 
15
- import { navigationItemType } from '../../utils/enums';
16
+ import { navigationItemAdditionalFields, navigationItemType } from '../../utils/enums';
16
17
  import slugify from 'slugify';
17
18
  import { extractRelatedItemLabel } from '../../utils/parsers';
18
19
  import { form as formDefinition } from './utils/form';
19
20
  import { checkFormValidity } from '../../utils/form';
20
- import { getTradId } from '../../../../translations';
21
+ import { getTradId, getTrad } from '../../../../translations';
21
22
 
22
23
  const NavigationItemForm = ({
23
24
  isLoading,
@@ -52,20 +53,29 @@ const NavigationItemForm = ({
52
53
  ...data,
53
54
  type: data.type || navigationItemType.INTERNAL,
54
55
  related: data.related?.value,
55
- relatedType: data.relatedType?.value
56
+ relatedType: data.relatedType?.value,
57
+ audience: data.audience?.map(item => item.id),
56
58
  });
57
59
  }
58
60
 
61
+ const audience = get(form, `${inputsPrefix}audience`, []);
62
+ const audienceOptions = availableAudience.map((item) => ({
63
+ value: get(item, 'id', " "),
64
+ label: get(item, 'name', " "),
65
+ }));
66
+
59
67
  const generatePreviewPath = () => {
60
68
  if (!isExternal) {
69
+ const value = `${data.levelPath !== '/' ? `${data.levelPath}` : ''}/${form.path !== '/' ? form.path || '' : ''}`;
61
70
  return {
62
- id: `${data.levelPath !== '/' ? `${data.levelPath}` : ''}/${form.path || ''}`,
63
- defaultMessage: `${data.levelPath !== '/' ? `${data.levelPath}` : ''}/${form.path || ''}`
71
+ id: getTradId('popup.item.form.type.external.description'),
72
+ defaultMessage: '',
73
+ values: { value }
64
74
  }
65
75
  }
66
76
  return null;
67
77
  };
68
-
78
+
69
79
  const sanitizePayload = (payload = {}) => {
70
80
  const { onItemClick, onItemLevelAddClick, related, relatedType, menuAttached, ...purePayload } = payload;
71
81
  const sanitizedType = purePayload.type || navigationItemType.INTERNAL;
@@ -101,6 +111,10 @@ const NavigationItemForm = ({
101
111
  const onTypeChange = ({ target: { name, value } }) =>
102
112
  onChange({ target: { name, value: value ? navigationItemType.INTERNAL : navigationItemType.EXTERNAL } });
103
113
 
114
+ const onAudienceChange = (value) => {
115
+ onChange({target: {name: `${inputsPrefix}audience`, value}});
116
+ }
117
+
104
118
  const onChange = ({ target: { name, value } }) => {
105
119
  setFormState(prevState => ({
106
120
  ...prevState,
@@ -137,7 +151,7 @@ const NavigationItemForm = ({
137
151
  .filter((item) => {
138
152
  const usedContentTypeEntitiesOfSameType = usedContentTypeEntities
139
153
  .filter(uctItem => relatedTypeSelectValue === uctItem.__collectionUid);
140
- return !find(usedContentTypeEntitiesOfSameType, uctItem => (item.id === uctItem.id && uctItem.id !== form.related));
154
+ return !find(usedContentTypeEntitiesOfSameType, uctItem => (item.id === uctItem.id && uctItem.id !== form.related));
141
155
  })
142
156
  .map((item) => {
143
157
  const label = appendLabelPublicationStatus(
@@ -257,6 +271,10 @@ const NavigationItemForm = ({
257
271
  }}
258
272
  name={`${inputsPrefix}title`}
259
273
  placeholder={{
274
+ id: "e.g. Blog",
275
+ defaultMessage: 'e.g. Blog',
276
+ }}
277
+ description={{
260
278
  id: getTradId('popup.item.form.title.placeholder'),
261
279
  defaultMessage: 'e.g. Blog',
262
280
  }}
@@ -329,6 +347,15 @@ const NavigationItemForm = ({
329
347
  onChange={onChangeRelatedType}
330
348
  options={relatedTypeSelectOptions}
331
349
  value={relatedTypeSelectValue}
350
+ disabled={isLoading || isEmpty(relatedTypeSelectOptions)}
351
+ description={
352
+ !isLoading && isEmpty(relatedTypeSelectOptions)
353
+ ? {
354
+ id: getTradId('popup.item.form.relatedType.empty'),
355
+ defaultMessage: 'There are no more content types',
356
+ }
357
+ : undefined
358
+ }
332
359
  />
333
360
  </GridItem>
334
361
  {relatedTypeSelectValue && !isSingleSelected && (
@@ -365,6 +392,22 @@ const NavigationItemForm = ({
365
392
  )}
366
393
  </>
367
394
  )}
395
+
396
+ {additionalFields.includes(navigationItemAdditionalFields.AUDIENCE) && (
397
+ <GridItem key={`${inputsPrefix}audience`} col={6} lg={12}>
398
+ <Select
399
+ id={`${inputsPrefix}audience`}
400
+ placeholder={formatMessage(getTrad('popup.item.form.audience.placeholder'))}
401
+ label={formatMessage(getTrad('popup.item.form.audience.label'))}
402
+ onChange={onAudienceChange}
403
+ value={audience}
404
+ multi
405
+ withTags
406
+ >
407
+ {audienceOptions.map(({ value, label }) => <Option key={value} value={value}>{label}</Option>)}
408
+ </Select>
409
+ </GridItem>
410
+ )}
368
411
  </Grid>
369
412
  </ModalBody>
370
413
  </Form>
@@ -6,14 +6,12 @@ import { ModalHeader } from '@strapi/design-system/ModalLayout';
6
6
  import { useIntl } from 'react-intl';
7
7
  import { getTrad } from '../../../../translations';
8
8
 
9
- export const NavigationItemPopupHeader = () => {
9
+ export const NavigationItemPopupHeader = ({isNewItem}) => {
10
10
  const { formatMessage } = useIntl();
11
11
  return (
12
12
  <ModalHeader>
13
13
  <ButtonText textColor="neutral800" as="h2" id="asset-dialog-title">
14
- {formatMessage(
15
- getTrad('popup.item.header'),
16
- )}
14
+ {formatMessage(getTrad(`popup.item.header.${isNewItem ? 'new' : 'edit'}`))}
17
15
  </ButtonText>
18
16
  </ModalHeader>
19
17
  );
@@ -80,7 +80,7 @@ const NavigationItemPopUp = ({
80
80
 
81
81
  return (
82
82
  <ModalLayout labelledBy="condition-modal-breadcrumbs" onClose={onClose} isOpen={isOpen}>
83
- <NavigationItemPopupHeader />
83
+ <NavigationItemPopupHeader isNewItem={!data.viewId}/>
84
84
  <NavigationItemForm
85
85
  data={prepareFormData(data)}
86
86
  isLoading={isLoading}
@@ -10,10 +10,13 @@ import { isEmpty, get } from "lodash";
10
10
 
11
11
  // Design System
12
12
  import { Main } from '@strapi/design-system/Main';
13
+ import { Flex } from '@strapi/design-system/Flex';
13
14
  import { ContentLayout } from '@strapi/design-system/Layout';
15
+ import { Typography } from '@strapi/design-system/Typography';
16
+ import { Box } from '@strapi/design-system/Box';
17
+ import { Icon } from '@strapi/design-system/Icon';
14
18
  import { Button } from '@strapi/design-system/Button';
15
19
  import { LoadingIndicatorPage } from "@strapi/helper-plugin";
16
- import { EmptyStateLayout } from '@strapi/design-system/EmptyStateLayout';
17
20
  import EmptyDocumentsIcon from '@strapi/icons/EmptyDocuments';
18
21
  import PlusIcon from "@strapi/icons/Plus";
19
22
 
@@ -22,6 +25,7 @@ import List from '../../components/NavigationItemList';
22
25
  import NavigationContentHeader from './components/NavigationContentHeader';
23
26
  import NavigationHeader from './components/NavigationHeader';
24
27
  import NavigationItemPopUp from "./components/NavigationItemPopup";
28
+ import Search from '../../components/Search';
25
29
  import useDataManager from "../../hooks/useDataManager";
26
30
  import { getTrad } from '../../translations';
27
31
  import {
@@ -30,7 +34,6 @@ import {
30
34
  usedContentTypes,
31
35
  validateNavigationStructure,
32
36
  } from './utils/parsers';
33
- import Search from '../../components/Search';
34
37
 
35
38
  const View = () => {
36
39
  const {
@@ -55,6 +58,7 @@ const View = () => {
55
58
  const { formatMessage } = useIntl();
56
59
 
57
60
  const [searchValue, setSearchValue] = useState('');
61
+ const [structureChanged, setStructureChanged] = useState(false);
58
62
  const isSearchEmpty = isEmpty(searchValue);
59
63
 
60
64
  const structureHasErrors = !validateNavigationStructure((changedActiveNavigation || {}).items);
@@ -104,6 +108,7 @@ const View = () => {
104
108
  items: transformItemToViewPayload(payload, changedActiveNavigation.items, config),
105
109
  };
106
110
  handleChangeNavigationData(changedStructure, true);
111
+ setStructureChanged(true);
107
112
  };
108
113
 
109
114
  const filteredListFactory = (items, filterFunction) => items.reduce((acc, item) => {
@@ -147,10 +152,19 @@ const View = () => {
147
152
  changeNavigationItemPopupState(false);
148
153
  };
149
154
 
155
+ const handleChangeNavigationSelection = (...args) => {
156
+ handleChangeSelection(...args);
157
+ setSearchValue('');
158
+ }
159
+
150
160
  return (
151
161
  <Main labelledBy="title" aria-busy={isLoadingForSubmit}>
152
162
  <NavigationHeader
153
163
  structureHasErrors={structureHasErrors}
164
+ structureHasChanged={structureChanged}
165
+ availableNavigations={availableNavigations}
166
+ activeNavigation={activeNavigation}
167
+ handleChangeSelection={handleChangeNavigationSelection}
154
168
  handleSave={handleSave}
155
169
  />
156
170
  <ContentLayout>
@@ -158,7 +172,7 @@ const View = () => {
158
172
  {changedActiveNavigation && (
159
173
  <>
160
174
  <NavigationContentHeader
161
- startActions={<Search value={searchValue} setValue={setSearchValue}/>}
175
+ startActions={<Search value={searchValue} setValue={setSearchValue} />}
162
176
  endActions={<Button
163
177
  onClick={addNewNavigationItem}
164
178
  startIcon={<PlusIcon />}
@@ -169,20 +183,20 @@ const View = () => {
169
183
  </Button>}
170
184
  />
171
185
  {isEmpty(changedActiveNavigation.items || []) && (
172
- <EmptyStateLayout
173
- action={
174
- <Button
175
- variant='secondary'
176
- startIcon={<PlusIcon />}
177
- label={formatMessage(getTrad('empty.cta'))}
178
- onClick={addNewNavigationItem}
179
- >
180
- {formatMessage(getTrad('empty.cta'))}
181
- </Button>
182
- }
183
- icon={<EmptyDocumentsIcon width='10rem' />}
184
- content={formatMessage(getTrad('empty'))}
185
- />
186
+ <Flex direction="column" minHeight="400px" justifyContent="center">
187
+ <Icon as={EmptyDocumentsIcon} width="160px" height="88px" color=""/>
188
+ <Box padding={4}>
189
+ <Typography variant="beta" textColor="neutral600">{formatMessage(getTrad('empty'))}</Typography>
190
+ </Box>
191
+ <Button
192
+ variant='secondary'
193
+ startIcon={<PlusIcon />}
194
+ label={formatMessage(getTrad('empty.cta'))}
195
+ onClick={addNewNavigationItem}
196
+ >
197
+ {formatMessage(getTrad('empty.cta'))}
198
+ </Button>
199
+ </Flex>
186
200
  )}
187
201
  {
188
202
  !isEmpty(changedActiveNavigation.items || [])
@@ -7,6 +7,7 @@ export const transformItemToRESTPayload = (
7
7
  parent = undefined,
8
8
  master = undefined,
9
9
  config = {},
10
+ parentAttachedToMenu = true,
10
11
  ) => {
11
12
  const {
12
13
  id,
@@ -34,7 +35,7 @@ export const transformItemToRESTPayload = (
34
35
  find(contentTypes,
35
36
  ct => ct.uid === relatedType) :
36
37
  undefined;
37
-
38
+ const itemAttachedToMenu = menuAttached && parentAttachedToMenu
38
39
  return {
39
40
  id,
40
41
  parent,
@@ -45,7 +46,7 @@ export const transformItemToRESTPayload = (
45
46
  removed,
46
47
  order,
47
48
  uiRouterKey,
48
- menuAttached,
49
+ menuAttached: itemAttachedToMenu,
49
50
  audience: audience.map((audienceItem) =>
50
51
  isObject(audienceItem) ? audienceItem.value : audienceItem,
51
52
  ),
@@ -60,7 +61,7 @@ export const transformItemToRESTPayload = (
60
61
  field: relatedContentType && relatedContentType.relatedField ? relatedContentType.relatedField : 'navigation',
61
62
  },
62
63
  ],
63
- items: items.map((iItem) => transformItemToRESTPayload(iItem, id, master, config)),
64
+ items: items.map((iItem) => transformItemToRESTPayload(iItem, id, master, config, itemAttachedToMenu)),
64
65
  };
65
66
  };
66
67
 
@@ -118,6 +119,7 @@ const linkRelations = (item, config) => {
118
119
 
119
120
  const shouldFindRelated = (isNumber(related) || isUuid(related) || isString(related)) && !relatedRef;
120
121
  const shouldBuildRelated = !relatedRef || (relatedRef && (relatedRef.id !== relatedId));
122
+
121
123
  if (shouldBuildRelated && !shouldFindRelated) {
122
124
  const relatedContentType = find(contentTypes,
123
125
  ct => ct.uid === relatedItem.__contentType, {});
@@ -136,6 +138,7 @@ const linkRelations = (item, config) => {
136
138
  const relatedRef = find(contentTypeItems, cti => cti.id === relatedId);
137
139
  const relatedContentType = find(contentTypes, ct => ct.uid === relatedType);
138
140
  const { uid, contentTypeName, labelSingular, isSingle } = relatedContentType;
141
+
139
142
  relation = {
140
143
  relatedRef: {
141
144
  ...relatedRef,
@@ -249,11 +252,14 @@ export const prepareItemToViewPayload = (items = [], viewParentId = null, config
249
252
  }));
250
253
 
251
254
  export const extractRelatedItemLabel = (item = {}, fields = {}, config = {}) => {
255
+ if (get(item, 'isSingle', false)) {
256
+ return get(item, 'labelSingular', '');
257
+ }
252
258
  const { contentTypes = [] } = config;
253
259
  const { __collectionUid } = item;
254
260
  const contentType = contentTypes.find(_ => _.uid === __collectionUid)
255
261
  const { default: defaultFields = [] } = fields;
256
- return get(fields, `${contentType ? contentType.collectionName : ''}`, defaultFields).map((_) => item[_]).filter((_) => _)[0] || '';
262
+ return get(fields, `${contentType ? contentType.uid : __collectionUid}`, defaultFields).map((_) => item[_]).filter((_) => _)[0] || '';
257
263
  };
258
264
 
259
265
  export const usedContentTypes = (items = []) => items.flatMap(
@@ -0,0 +1,8 @@
1
+ const permissions = require('./../../permissions');
2
+
3
+ const pluginPermissions = {
4
+ access: [{ action: permissions.render(permissions.navigation.read), subject: null }],
5
+ update: [{ action: permissions.render(permissions.navigation.update), subject: null }],
6
+ };
7
+
8
+ export default pluginPermissions;
@@ -5,9 +5,10 @@
5
5
  "header.action.newItem": "New Item",
6
6
  "submit.cta.cancel": "Cancel",
7
7
  "submit.cta.save": "Save",
8
- "empty": "Navigation container is empty",
8
+ "empty": "Your navigation is empty",
9
9
  "empty.cta": "Create first item",
10
- "popup.item.header": "Manage navigation item",
10
+ "popup.item.header.edit": "Edit navigation item",
11
+ "popup.item.header.new": "New navigation item",
11
12
  "popup.item.form.title.label": "Title",
12
13
  "popup.item.form.title.placeholder": "Enter the item title or leave blank to pull from related entity",
13
14
  "popup.item.form.uiRouterKey.label": "UI router key",
@@ -22,11 +23,13 @@
22
23
  "popup.item.form.type.label": "Internal link",
23
24
  "popup.item.form.type.internal.label": "Internal source",
24
25
  "popup.item.form.type.external.label": "External source",
26
+ "popup.item.form.type.external.description": "Output path: {value}",
25
27
  "popup.item.form.audience.label": "Audience",
26
- "popup.item.form.audience.placeholder": "Type to start searching...",
28
+ "popup.item.form.audience.placeholder": "Select audience...",
27
29
  "popup.item.form.relatedSection.label": "Relation to",
28
30
  "popup.item.form.relatedType.label": "Content Type",
29
31
  "popup.item.form.relatedType.placeholder": "Select content type...",
32
+ "popup.item.form.relatedType.empty": "There are no content types to select",
30
33
  "popup.item.form.related.label": "Entity",
31
34
  "popup.item.form.related.empty": "There are no more entities of \"{ contentTypeName }\" to select",
32
35
  "popup.item.form.button.create": "Create item",
@@ -40,9 +43,9 @@
40
43
  "notification.navigation.item.relation": "Entity relation does not exist!",
41
44
  "notification.navigation.item.relation.status.draft": "draft",
42
45
  "notification.navigation.item.relation.status.published": "published",
43
- "navigation.item.action.newItem": "New nested item",
46
+ "navigation.item.action.newItem": "Add nested item",
44
47
  "navigation.item.badge.removed": "Removed",
45
- "navigation.item.badge.draft": "Draft",
46
- "navigation.item.badge.published": "Published"
48
+ "navigation.item.badge.draft": "{type}: Draft",
49
+ "navigation.item.badge.published": "{type}: Published"
47
50
  }
48
51
 
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "strapi-plugin-navigation",
3
- "version": "2.0.0-beta.4",
3
+ "version": "2.0.1",
4
4
  "description": "Strapi - Navigation plugin",
5
5
  "strapi": {
6
6
  "name": "navigation",
7
- "icon": "hamburger",
8
- "description": "UI navigation management",
7
+ "displayName": "Navigation",
8
+ "description": "Create consumable navigation with a simple and straightforward visual builder",
9
9
  "kind": "plugin"
10
10
  },
11
11
  "repository": {
@@ -13,9 +13,11 @@
13
13
  "url": "https://github.com/VirtusLab/strapi-plugin-navigation"
14
14
  },
15
15
  "scripts": {
16
+ "publish": "npm publish --tag latest",
16
17
  "test:unit": "jest --verbose --coverage"
17
18
  },
18
19
  "dependencies": {
20
+ "@strapi/utils": "^4.0.7",
19
21
  "uuid": "^8.3.0",
20
22
  "bad-words": "^3.0.3",
21
23
  "lodash": "^4.17.11",
package/permissions.js ADDED
@@ -0,0 +1,11 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ render: function(uid) {
5
+ return `plugin::navigation.${uid}`;
6
+ },
7
+ navigation: {
8
+ read: 'read',
9
+ update: 'update',
10
+ },
11
+ };
@@ -1,4 +1,5 @@
1
1
  const { isEmpty } = require("lodash");
2
+ const permissions = require('./../permissions');
2
3
 
3
4
  module.exports = async ({ strapi }) => {
4
5
  // Check if the plugin users-permissions is installed because the navigation needs it
@@ -11,14 +12,14 @@ module.exports = async ({ strapi }) => {
11
12
  const actions = [
12
13
  {
13
14
  section: "plugins",
14
- displayName: "Access the Navigation",
15
- uid: "read",
15
+ displayName: "Read",
16
+ uid: permissions.navigation.read,
16
17
  pluginName: "navigation",
17
18
  },
18
19
  {
19
20
  section: "plugins",
20
- displayName: "Ability to change the Navigation",
21
- uid: "update",
21
+ displayName: "Update",
22
+ uid: permissions.navigation.update,
22
23
  pluginName: "navigation",
23
24
  },
24
25
  ];