strapi-plugin-navigation 2.0.0-beta.5 → 2.0.0-rc.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.
package/README.md CHANGED
@@ -1,22 +1,27 @@
1
1
  <div align="center">
2
- <h1>Strapi v4 - Navigation plugin - BETA</h1>
3
- <p>Create consumable navigation with a simple and straigthforward visual builder.</p>
2
+ <img style="width: 150px; height: auto;" src="public/assets/logo.png" alt="Logo - Strapi Navigation plugin" />
3
+ <h1>Strapi v4 - Navigation plugin</h1>
4
+ <p>Create consumable navigation with a simple and straighthforward visual builder</p>
4
5
  <a href="https://www.npmjs.org/package/strapi-plugin-navigation">
5
- <img src="https://img.shields.io/github/package-json/v/VirtusLab-Open-Source/strapi-plugin-navigation/feat%252Fstrapi-v4-support?label=npm" alt="NPM Version" />
6
+ <img alt="GitHub package.json version" src="https://img.shields.io/github/package-json/v/VirtusLab-Open-Source/strapi-plugin-navigation?label=npm&logo=npm">
6
7
  </a>
7
8
  <a href="https://www.npmjs.org/package/strapi-plugin-navigation">
8
9
  <img src="https://img.shields.io/npm/dm/strapi-plugin-navigation.svg" alt="Monthly download on NPM" />
9
10
  </a>
10
11
  <a href="https://circleci.com/gh/VirtusLab/strapi-plugin-navigation">
11
- <img src="https://circleci.com/gh/VirtusLab-Open-Source/strapi-plugin-navigation/tree/feat%2Fstrapi-v4-support.svg?style=shield" alt="CircleCI" />
12
+ <img src="https://circleci.com/gh/VirtusLab-Open-Source/strapi-plugin-navigation.svg?style=shield" alt="CircleCI" />
12
13
  </a>
13
14
  <a href="https://codecov.io/gh/VirtusLab/strapi-plugin-navigation">
14
- <img src="https://codecov.io/gh/VirtusLab/strapi-plugin-navigation/coverage.svg?branch=feat%2Fstrapi-v4-support" alt="codecov.io" />
15
+ <img src="https://codecov.io/gh/VirtusLab/strapi-plugin-navigation/coverage.svg?branch=master" alt="codecov.io" />
15
16
  </a>
16
17
  </div>
17
18
 
18
19
  ---
19
20
 
21
+ <div style="margin: 20px 0" align="center">
22
+ <img style="width: 100%; height: auto;" src="public/assets/preview.png" alt="UI preview" />
23
+ </div>
24
+
20
25
  Strapi Navigation Plugin provides a website navigation / menu builder feature for [Strapi Headless CMS](https://github.com/strapi/strapi) admin panel. Navigation has the possibility to control the audience and can be consumed by the website with different output structure renderers:
21
26
 
22
27
  - Flat
@@ -34,8 +39,8 @@ Strapi Navigation Plugin provides a website navigation / menu builder feature fo
34
39
 
35
40
  ## ⚙️ Versions
36
41
 
37
- - **Stable** - [v1.1.2](https://github.com/VirtusLab-Open-Source/strapi-plugin-navigation)
38
- - **Beta** - v4 support - [v2.0.0-beta.x](https://github.com/VirtusLab-Open-Source/strapi-plugin-navigation/tree/feat/strapi-v4-support)
42
+ - **Strapi v4** - (current) - [v2.x](https://github.com/VirtusLab-Open-Source/strapi-plugin-navigation)
43
+ - **Strapi v3** - [v1.x](https://github.com/VirtusLab-Open-Source/strapi-plugin-navigation/tree/strapi-v3)
39
44
 
40
45
  ## ⏳ Installation
41
46
 
@@ -71,9 +76,7 @@ Complete installation requirements are exact same as for Strapi itself and can b
71
76
  - Strapi v4.0.5 (recently tested)
72
77
  - Strapi v4.x
73
78
 
74
- _This plugin is not working with v3.x._
75
-
76
- It may or may not work with the older Strapi v4 versions, these are not tested nor officially supported at this time.
79
+ > This plugin is designed for **Strapi v4** and is not working with v3.x. To get version for **Strapi v3** install version [v1.x](https://github.com/VirtusLab-Open-Source/strapi-plugin-navigation/tree/strapi-v3).
77
80
 
78
81
  **We recommend always using the latest version of Strapi to start your new projects**.
79
82
 
@@ -105,7 +108,7 @@ Config for this plugin is stored as a part of `config/plugins.js` or `config/<en
105
108
  - `contentTypesNameFields` - Definition of content type title fields like `'api::<collection name>.<content type name>': ['field_name_1', 'field_name_2']`, if not set titles are pulled from fields like `['title', 'subject', 'name']`. **TIP** - Proper content type uid you can find in the URL of Content Manager where you're managing relevant entities like: `admin/content-manager/collectionType/< THE UID HERE >?page=1&pageSize=10&sort=Title:ASC&plugins[i18n][locale]=en`
106
109
  - `gql` - If you're using GraphQL that's the right place to put all necessary settings. More **[ here ](#gql-configuration)**
107
110
 
108
- ## GQL Configuration
111
+ ## 🔧 GQL Configuration
109
112
  Using navigation with GraphQL requires both plugins to be installed and working. You can find instalation guide for GraphQL plugin **[here](https://docs.strapi.io/developer-docs/latest/plugins/graphql.html#graphql)**. To properly configure GQL to work with navigation you should provide `gql` prop. This should contain union types that will be used to define GQL response format for your data while fetching:
110
113
 
111
114
  ```gql
@@ -131,8 +134,14 @@ gql: {
131
134
  ```
132
135
  where `Page` and `UploadFile` are your type names for the **Content Types** you're referring by navigation items relations.
133
136
 
137
+ ## 👤 RBAC
138
+ Plugin provides granular permissions based on Strapi RBAC functionality.
139
+
140
+ ### Mandatory permissions
141
+ For any role different than **Super Admin**, to access the **Navigation panel** you must set following permissions:
142
+ - _Plugins_ -> _Navigation_ -> _Read_ - gives you the access to **Navigation Panel**
134
143
 
135
- ## Public API Navigation Item model
144
+ ## Base Navigation Item model
136
145
 
137
146
  ### Flat
138
147
  ```
@@ -146,8 +155,8 @@ where `Page` and `UploadFile` are your type names for the **Content Types** you'
146
155
  "menuAttached": false,
147
156
  "parent": 8, // Parent Navigation Item 'id', null in case of root level
148
157
  "master": 1, // Navigation 'id'
149
- "created_at": "2020-09-29T13:29:19.086Z",
150
- "updated_at": "2020-09-29T13:29:19.128Z",
158
+ "createdAt": "2020-09-29T13:29:19.086Z",
159
+ "updatedAt": "2020-09-29T13:29:19.128Z",
151
160
  "related": [ <Content Type model > ],
152
161
  "audience": []
153
162
  }
@@ -198,7 +207,7 @@ where `Page` and `UploadFile` are your type names for the **Content Types** you'
198
207
  }
199
208
  ```
200
209
 
201
- ## Public API specification
210
+ ## 🕸️ Public API specification
202
211
 
203
212
  ### Render
204
213
 
@@ -387,7 +396,8 @@ For general help using Strapi, please refer to [the official Strapi documentatio
387
396
  - [Slack](http://slack.strapi.io) We're present on official Strapi slack workspace. Look for @cyp3r and DM.
388
397
  - [Slack - VirtusLab Open Source](https://virtuslab-oss.slack.com) We're present on a public channel #strapi-molecules
389
398
  - [GitHub](https://github.com/VirtusLab/strapi-plugin-navigation/issues) (Bug reports, Contributions, Questions and Discussions)
399
+ - [E-mail](mailto:strapi@virtuslab.com) - we will respond back as soon as possible
390
400
 
391
401
  ## 📝 License
392
402
 
393
- [MIT License](LICENSE.md) Copyright (c) 2021 [VirtusLab Sp. z o.o.](https://virtuslab.com/) &amp; [Strapi Solutions](https://strapi.io/).
403
+ [MIT License](LICENSE.md) Copyright (c) [VirtusLab Sp. z o.o.](https://virtuslab.com/) &amp; [Strapi Solutions](https://strapi.io/).
@@ -10,7 +10,8 @@ const EmptyView = styled.div`
10
10
  justify-content: center;
11
11
  padding-left: 2rem;
12
12
  padding-right: 2rem;
13
- padding-bottom: "8rem" };
13
+ padding-bottom: 8rem;
14
+
14
15
 
15
16
  font-size: 2rem;
16
17
  font-weight: 600;
@@ -2,7 +2,20 @@ import styled from "styled-components";
2
2
  import { Badge } from '@strapi/design-system/Badge';
3
3
 
4
4
  const ItemCardBadge = styled(Badge)`
5
- border: 1px solid ${({ theme, borderColor }) => theme.colors[borderColor]}
5
+ border: 1px solid ${({ theme, borderColor }) => theme.colors[borderColor]};
6
+
7
+ ${ props => props.small && `
8
+ padding: 0 4px;
9
+ vertical-align: middle;
10
+
11
+ cursor: default;
12
+
13
+ span {
14
+ font-size: .55rem;
15
+ line-height: 1;
16
+ vertical-align: middle;
17
+ }
18
+ `}
6
19
  `;
7
20
 
8
21
  export default ItemCardBadge;
@@ -12,8 +12,8 @@ import Wrapper from './Wrapper';
12
12
  import ItemCardBadge from '../ItemCardBadge';
13
13
  import { getTrad } from "../../../translations";
14
14
 
15
- const ItemCardHeader = ({ title, path, icon, removed, isExternal, isPublished, onItemRemove, onItemEdit, onItemRestore }) => {
16
- const badgeColor = isPublished ? 'success' : 'secondary';
15
+ const ItemCardHeader = ({ title, path, icon, removed, onItemRemove, onItemEdit, onItemRestore }) => {
16
+
17
17
  const { formatMessage } = useIntl();
18
18
 
19
19
  return (
@@ -27,23 +27,15 @@ const ItemCardHeader = ({ title, path, icon, removed, isExternal, isPublished, o
27
27
  {path}
28
28
  </Typography>
29
29
  </Flex>
30
- <Flex alignItems="center">
31
- {removed ?
32
- <ItemCardBadge
30
+ <Flex alignItems="center" style={{ zIndex: 2 }}>
31
+ {removed &&
32
+ (<ItemCardBadge
33
33
  borderColor={`danger200`}
34
34
  backgroundColor={`danger100`}
35
35
  textColor={`danger600`}
36
36
  >
37
37
  {formatMessage(getTrad("navigation.item.badge.removed"))}
38
- </ItemCardBadge>
39
- : !isExternal && <ItemCardBadge
40
- borderColor={`${badgeColor}200`}
41
- backgroundColor={`${badgeColor}100`}
42
- textColor={`${badgeColor}600`}
43
- className="action"
44
- >
45
- {formatMessage(getTrad(`navigation.item.badge.${isPublished ? 'published' : 'draft'}`))}
46
- </ItemCardBadge>
38
+ </ItemCardBadge>)
47
39
  }
48
40
 
49
41
  <IconButton disabled={removed} onClick={onItemEdit} label="Edit" icon={<PencilIcon />} />
@@ -0,0 +1,12 @@
1
+ import styled from "styled-components";
2
+
3
+ export const ItemCardRemovedOverlay = styled.div`
4
+ width: 100%;
5
+ height: 100%;
6
+ position: absolute;
7
+ left: 0;
8
+ right: 0;
9
+ z-index: 1;
10
+
11
+ background: rgba(255,255,255,.75);
12
+ `;
@@ -1,21 +1,26 @@
1
1
  import PropTypes from 'prop-types';
2
2
  import React from 'react';
3
- import { isEmpty, isNumber } from 'lodash';
3
+ import { isEmpty, isNumber, get } from 'lodash';
4
4
  import { useIntl } from "react-intl";
5
5
 
6
+ import { Box } from '@strapi/design-system/Box';
6
7
  import { Card, CardBody } from '@strapi/design-system/Card';
7
8
  import { Divider } from '@strapi/design-system/Divider';
9
+ import { Flex } from '@strapi/design-system/Flex';
10
+ import { Link } from '@strapi/design-system/Link';
8
11
  import { TextButton } from '@strapi/design-system/TextButton';
9
12
  import { Typography } from '@strapi/design-system/Typography';
10
- import PlusIcon from '@strapi/icons/Plus';
11
- import EarthIcon from '@strapi/icons/Earth';
12
- import LinkIcon from '@strapi/icons/Link';
13
+
14
+ import { ArrowRight, Link as LinkIcon, Earth, Plus } from '@strapi/icons';
13
15
 
14
16
  import { navigationItemType } from '../../pages/View/utils/enums';
15
17
  import ItemCardHeader from './ItemCardHeader';
16
18
  import List from '../NavigationItemList';
17
19
  import Wrapper from './Wrapper';
18
20
  import { getTrad } from '../../translations';
21
+ import { extractRelatedItemLabel } from '../../pages/View/utils/parsers';
22
+ import ItemCardBadge from './ItemCardBadge';
23
+ import { ItemCardRemovedOverlay } from './ItemCardRemovedOverlay';
19
24
 
20
25
  const Item = (props) => {
21
26
  const {
@@ -32,6 +37,7 @@ const Item = (props) => {
32
37
  onItemEdit,
33
38
  error,
34
39
  displayChildren,
40
+ config = {},
35
41
  } = props;
36
42
 
37
43
  const {
@@ -45,6 +51,7 @@ const Item = (props) => {
45
51
  } = item;
46
52
 
47
53
  const { formatMessage } = useIntl();
54
+ const { contentTypes, contentTypesNameFields } = config;
48
55
  const isExternal = type === navigationItemType.EXTERNAL;
49
56
  const isPublished = relatedRef && relatedRef?.publishedAt;
50
57
  const isNextMenuAllowedLevel = isNumber(allowedLevels) ? level < (allowedLevels - 1) : true;
@@ -52,38 +59,70 @@ const Item = (props) => {
52
59
  const hasChildren = !isEmpty(item.items) && !isExternal && !displayChildren;
53
60
  const absolutePath = isExternal ? undefined : `${levelPath === '/' ? '' : levelPath}/${path === '/' ? '' : path}`;
54
61
 
62
+ const relatedItemLabel = !isExternal ? extractRelatedItemLabel(relatedRef, contentTypesNameFields, { contentTypes }) : '';
63
+ const relatedTypeLabel = relatedRef?.labelSingular;
64
+ const relatedBadgeColor = isPublished ? 'success' : 'secondary';
65
+
55
66
  return (
56
67
  <Wrapper level={level} isLast={isLast}>
57
- <Card style={{ width: "728px", zIndex: 1, position: "relative" }}>
68
+ <Card style={{ width: "728px", zIndex: 1, position: "relative", overflow: 'hidden' }}>
69
+ { removed && (<ItemCardRemovedOverlay />) }
58
70
  <CardBody>
59
71
  <ItemCardHeader
60
72
  title={title}
61
73
  path={isExternal ? externalPath : absolutePath}
62
- icon={isExternal ? <EarthIcon /> : <LinkIcon />}
63
- isPublished={isPublished}
64
- isExternal={isExternal}
65
- onItemRemove={() => onItemRemove(item)}
74
+ icon={isExternal ? <Earth /> : <LinkIcon />}
75
+ onItemRemove={() => onItemRemove({
76
+ ...item,
77
+ relatedRef,
78
+ })}
66
79
  onItemEdit={() => onItemEdit({
67
80
  ...item,
68
81
  isMenuAllowedLevel,
69
82
  isParentAttachedToMenu,
70
83
  }, levelPath, isParentAttachedToMenu)}
71
- onItemRestore={() => onItemRestore(item)}
84
+ onItemRestore={() => onItemRestore({
85
+ ...item,
86
+ relatedRef,
87
+ })}
72
88
  removed={removed}
73
89
  />
74
90
  </CardBody>
75
91
  <Divider />
76
- <CardBody style={{ margin: '8px' }}>
77
- <TextButton
78
- disabled={removed}
79
- startIcon={<PlusIcon />}
80
- onClick={(e) => onItemLevelAdd(e, viewId, isNextMenuAllowedLevel, absolutePath, menuAttached)}
81
- >
82
- <Typography variant="pi" fontWeight="bold" textColor={removed ? "neutral600" : "primary600"}>
83
- {formatMessage(getTrad("navigation.item.action.newItem"))}
84
- </Typography>
85
- </TextButton>
86
- </CardBody>
92
+ { !isExternal && (<CardBody style={{ margin: '8px' }}>
93
+ <Flex style={{ width: '100%' }} direction="row" alignItems="center" justifyContent="space-between">
94
+ <TextButton
95
+ disabled={removed}
96
+ startIcon={<Plus />}
97
+ onClick={(e) => onItemLevelAdd(e, viewId, isNextMenuAllowedLevel, absolutePath, menuAttached)}
98
+ >
99
+ <Typography variant="pi" fontWeight="bold" textColor={removed ? "neutral600" : "primary600"}>
100
+ {formatMessage(getTrad("navigation.item.action.newItem"))}
101
+ </Typography>
102
+ </TextButton>
103
+ { relatedItemLabel && (<Box>
104
+ <ItemCardBadge
105
+ style={{ marginRight: 4 }}
106
+ borderColor={`${relatedBadgeColor}200`}
107
+ backgroundColor={`${relatedBadgeColor}100`}
108
+ textColor={`${relatedBadgeColor}600`}
109
+ className="action"
110
+ small
111
+ >
112
+ {formatMessage(getTrad(`navigation.item.badge.${isPublished ? 'published' : 'draft'}`), {
113
+ type: relatedTypeLabel
114
+ })}
115
+ </ItemCardBadge>
116
+ <Typography variant="pi" fontWeight="bold" textColor="neutral600">
117
+ { relatedItemLabel }
118
+ <Link
119
+ to={`/content-manager/collectionType/${relatedRef?.__collectionUid}/${relatedRef?.id}`}
120
+ endIcon={<ArrowRight />}>&nbsp;</Link>
121
+ </Typography>
122
+ </Box>)
123
+ }
124
+ </Flex>
125
+ </CardBody>)}
87
126
  </Card>
88
127
  {hasChildren && !removed && <List
89
128
  onItemLevelAdd={onItemLevelAdd}
@@ -96,6 +135,8 @@ const Item = (props) => {
96
135
  items={item.items}
97
136
  level={level + 1}
98
137
  levelPath={absolutePath}
138
+ contentTypes={contentTypes}
139
+ contentTypesNameFields={contentTypesNameFields}
99
140
  />
100
141
  }
101
142
  </Wrapper>
@@ -121,6 +162,10 @@ Item.propTypes = {
121
162
  onItemRestore: PropTypes.func.isRequired,
122
163
  onItemLevelAdd: PropTypes.func.isRequired,
123
164
  onItemRemove: PropTypes.func.isRequired,
165
+ config: PropTypes.shape({
166
+ contentTypes: PropTypes.array.isRequired,
167
+ contentTypesNameFields: PropTypes.object.isRequired,
168
+ }).isRequired
124
169
  };
125
170
 
126
171
  export default Item;
@@ -16,6 +16,8 @@ const List = ({
16
16
  onItemRemove,
17
17
  onItemRestore,
18
18
  displayFlat,
19
+ contentTypes,
20
+ contentTypesNameFields,
19
21
  }) => (
20
22
  <Wrapper level={level}>
21
23
  {items.map((item, n) => {
@@ -36,6 +38,10 @@ const List = ({
36
38
  onItemEdit={onItemEdit}
37
39
  error={error}
38
40
  displayChildren={displayFlat}
41
+ config={{
42
+ contentTypes,
43
+ contentTypesNameFields
44
+ }}
39
45
  />
40
46
  );
41
47
  })}
@@ -51,6 +57,8 @@ List.propTypes = {
51
57
  onItemRemove: PropTypes.func.isRequired,
52
58
  onItemRestore: PropTypes.func.isRequired,
53
59
  onItemRestore: PropTypes.func.isRequired,
60
+ contentTypes: PropTypes.array.isRequired,
61
+ contentTypesNameFields: PropTypes.object.isRequired
54
62
  };
55
63
 
56
64
  export default List;
@@ -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,25 +3,15 @@ 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]};
9
+ import { MoreButton } from './styles';
16
10
 
17
- svg {
18
- width: ${18 / 16}rem;
19
- height: ${18 / 16}rem;
20
- }
21
- `;
22
11
 
23
12
  const NavigationHeader = ({
24
13
  structureHasErrors,
14
+ structureHAsChanged,
25
15
  handleSave,
26
16
  }) => {
27
17
  const { formatMessage } = useIntl();
@@ -33,16 +23,16 @@ const NavigationHeader = ({
33
23
  <Button
34
24
  onClick={handleSave}
35
25
  startIcon={<Check />}
36
- disabled={structureHasErrors}
26
+ disabled={structureHasErrors || !structureHAsChanged}
37
27
  type="submit"
38
28
  >
39
29
  {formatMessage(getTrad('submit.cta.save'))}
40
30
  </Button>
41
- <MoreButton
31
+ {/* <MoreButton
42
32
  id="more"
43
33
  label="More"
44
34
  icon={<More />}
45
- />
35
+ /> */}
46
36
  </Stack>
47
37
  }
48
38
  title={formatMessage({
@@ -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
+
@@ -58,9 +58,11 @@ const NavigationItemForm = ({
58
58
 
59
59
  const generatePreviewPath = () => {
60
60
  if (!isExternal) {
61
+ const value = `${data.levelPath !== '/' ? `${data.levelPath}` : ''}/${form.path !== '/' ? form.path || '' : ''}`;
61
62
  return {
62
- id: `${data.levelPath !== '/' ? `${data.levelPath}` : ''}/${form.path || ''}`,
63
- defaultMessage: `${data.levelPath !== '/' ? `${data.levelPath}` : ''}/${form.path || ''}`
63
+ id: getTradId('popup.item.form.type.external.description'),
64
+ defaultMessage: '',
65
+ values: { value }
64
66
  }
65
67
  }
66
68
  return null;
@@ -11,6 +11,7 @@ import { isEmpty, get } from "lodash";
11
11
  // Design System
12
12
  import { Main } from '@strapi/design-system/Main';
13
13
  import { ContentLayout } from '@strapi/design-system/Layout';
14
+ import { Box } from '@strapi/design-system/Box';
14
15
  import { Button } from '@strapi/design-system/Button';
15
16
  import { LoadingIndicatorPage } from "@strapi/helper-plugin";
16
17
  import { EmptyStateLayout } from '@strapi/design-system/EmptyStateLayout';
@@ -55,6 +56,7 @@ const View = () => {
55
56
  const { formatMessage } = useIntl();
56
57
 
57
58
  const [searchValue, setSearchValue] = useState('');
59
+ const [structureChanged, setStructureChanged] = useState(false);
58
60
  const isSearchEmpty = isEmpty(searchValue);
59
61
 
60
62
  const structureHasErrors = !validateNavigationStructure((changedActiveNavigation || {}).items);
@@ -104,6 +106,7 @@ const View = () => {
104
106
  items: transformItemToViewPayload(payload, changedActiveNavigation.items, config),
105
107
  };
106
108
  handleChangeNavigationData(changedStructure, true);
109
+ setStructureChanged(true);
107
110
  };
108
111
 
109
112
  const filteredListFactory = (items, filterFunction) => items.reduce((acc, item) => {
@@ -151,6 +154,7 @@ const View = () => {
151
154
  <Main labelledBy="title" aria-busy={isLoadingForSubmit}>
152
155
  <NavigationHeader
153
156
  structureHasErrors={structureHasErrors}
157
+ structureHAsChanged={structureChanged}
154
158
  handleSave={handleSave}
155
159
  />
156
160
  <ContentLayout>
@@ -168,21 +172,22 @@ const View = () => {
168
172
  {formatMessage(getTrad('header.action.newItem'))}
169
173
  </Button>}
170
174
  />
171
- {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
- />
175
+ {isEmpty(changedActiveNavigation.items || []) && (<Box paddingTop={4} >
176
+ <EmptyStateLayout
177
+ action={
178
+ <Button
179
+ variant='secondary'
180
+ startIcon={<PlusIcon />}
181
+ label={formatMessage(getTrad('empty.cta'))}
182
+ onClick={addNewNavigationItem}
183
+ >
184
+ {formatMessage(getTrad('empty.cta'))}
185
+ </Button>
186
+ }
187
+ icon={<EmptyDocumentsIcon width='10rem' />}
188
+ content={formatMessage(getTrad('empty'))}
189
+ />
190
+ </Box>
186
191
  )}
187
192
  {
188
193
  !isEmpty(changedActiveNavigation.items || [])
@@ -118,6 +118,7 @@ const linkRelations = (item, config) => {
118
118
 
119
119
  const shouldFindRelated = (isNumber(related) || isUuid(related) || isString(related)) && !relatedRef;
120
120
  const shouldBuildRelated = !relatedRef || (relatedRef && (relatedRef.id !== relatedId));
121
+
121
122
  if (shouldBuildRelated && !shouldFindRelated) {
122
123
  const relatedContentType = find(contentTypes,
123
124
  ct => ct.uid === relatedItem.__contentType, {});
@@ -136,6 +137,7 @@ const linkRelations = (item, config) => {
136
137
  const relatedRef = find(contentTypeItems, cti => cti.id === relatedId);
137
138
  const relatedContentType = find(contentTypes, ct => ct.uid === relatedType);
138
139
  const { uid, contentTypeName, labelSingular, isSingle } = relatedContentType;
140
+
139
141
  relation = {
140
142
  relatedRef: {
141
143
  ...relatedRef,
@@ -253,7 +255,7 @@ export const extractRelatedItemLabel = (item = {}, fields = {}, config = {}) =>
253
255
  const { __collectionUid } = item;
254
256
  const contentType = contentTypes.find(_ => _.uid === __collectionUid)
255
257
  const { default: defaultFields = [] } = fields;
256
- return get(fields, `${contentType ? contentType.collectionName : ''}`, defaultFields).map((_) => item[_]).filter((_) => _)[0] || '';
258
+ return get(fields, `${contentType ? contentType.uid : __collectionUid}`, defaultFields).map((_) => item[_]).filter((_) => _)[0] || '';
257
259
  };
258
260
 
259
261
  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;
@@ -22,6 +22,7 @@
22
22
  "popup.item.form.type.label": "Internal link",
23
23
  "popup.item.form.type.internal.label": "Internal source",
24
24
  "popup.item.form.type.external.label": "External source",
25
+ "popup.item.form.type.external.description": "Output path: {value}",
25
26
  "popup.item.form.audience.label": "Audience",
26
27
  "popup.item.form.audience.placeholder": "Type to start searching...",
27
28
  "popup.item.form.relatedSection.label": "Relation to",
@@ -42,7 +43,7 @@
42
43
  "notification.navigation.item.relation.status.published": "published",
43
44
  "navigation.item.action.newItem": "New nested item",
44
45
  "navigation.item.badge.removed": "Removed",
45
- "navigation.item.badge.draft": "Draft",
46
- "navigation.item.badge.published": "Published"
46
+ "navigation.item.badge.draft": "{type}: Draft",
47
+ "navigation.item.badge.published": "{type}: Published"
47
48
  }
48
49
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strapi-plugin-navigation",
3
- "version": "2.0.0-beta.5",
3
+ "version": "2.0.0-rc.1",
4
4
  "description": "Strapi - Navigation plugin",
5
5
  "strapi": {
6
6
  "name": "navigation",
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
+ };
Binary file
Binary file
@@ -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
  ];
@@ -1,5 +1,5 @@
1
1
  module.exports = ({ strapi, nexus }) => {
2
- const related = strapi.plugin('navigation').config('gql').navigationItemRelated;
2
+ const related = strapi.plugin('navigation').config('gql')?.navigationItemRelated;
3
3
  const name = "NavigationRelated";
4
4
 
5
5
  if (related?.length) {
@@ -700,7 +700,7 @@ module.exports = ({ strapi }) => {
700
700
  },
701
701
 
702
702
  removeRelated(relatedItems, master) {
703
- return Promise.all(relatedItems.map(relatedItem => {
703
+ return Promise.all((relatedItems || []).map(relatedItem => {
704
704
  const model = strapi.query('plugin::navigation.navigations-items-related');
705
705
  const entityToRemove = {
706
706
  master,
@@ -1,6 +0,0 @@
1
- import React from 'react';
2
- import Puzzle from '@strapi/icons/Puzzle';
3
-
4
- const PluginIcon = () => <Puzzle />;
5
-
6
- export default PluginIcon;