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 +26 -16
- package/admin/src/components/EmptyView/index.js +2 -1
- package/admin/src/components/Item/ItemCardBadge/index.js +14 -1
- package/admin/src/components/Item/ItemCardHeader/index.js +6 -14
- package/admin/src/components/Item/ItemCardRemovedOverlay/index.js +12 -0
- package/admin/src/components/Item/index.js +66 -21
- package/admin/src/components/NavigationItemList/index.js +8 -0
- package/admin/src/components/icons/navigation.js +14 -0
- package/admin/src/index.js +4 -3
- package/admin/src/pages/View/components/NavigationHeader/index.js +5 -15
- package/admin/src/pages/View/components/NavigationHeader/styles.js +13 -0
- package/admin/src/pages/View/components/NavigationItemForm/index.js +4 -2
- package/admin/src/pages/View/index.js +20 -15
- package/admin/src/pages/View/utils/parsers.js +3 -1
- package/admin/src/permissions.js +8 -0
- package/admin/src/translations/en.json +3 -2
- package/package.json +1 -1
- package/permissions.js +11 -0
- package/public/assets/logo.png +0 -0
- package/public/assets/preview.png +0 -0
- package/server/bootstrap.js +5 -4
- package/server/graphql/types/navigation-related.js +1 -1
- package/server/services/navigation.js +1 -1
- package/admin/src/components/PluginIcon/index.js +0 -6
package/README.md
CHANGED
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
<div align="center">
|
|
2
|
-
<
|
|
3
|
-
<
|
|
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
|
|
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
|
|
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=
|
|
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
|
-
- **
|
|
38
|
-
- **
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
"
|
|
150
|
-
"
|
|
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)
|
|
403
|
+
[MIT License](LICENSE.md) Copyright (c) [VirtusLab Sp. z o.o.](https://virtuslab.com/) & [Strapi Solutions](https://strapi.io/).
|
|
@@ -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,
|
|
16
|
-
|
|
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 />} />
|
|
@@ -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
|
-
|
|
11
|
-
import
|
|
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 ? <
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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(
|
|
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
|
-
<
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
{
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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 />}> </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;
|
package/admin/src/index.js
CHANGED
|
@@ -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:
|
|
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 {
|
|
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:
|
|
63
|
-
defaultMessage:
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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.
|
|
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
package/permissions.js
ADDED
|
Binary file
|
|
Binary file
|
package/server/bootstrap.js
CHANGED
|
@@ -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: "
|
|
15
|
-
uid:
|
|
15
|
+
displayName: "Read",
|
|
16
|
+
uid: permissions.navigation.read,
|
|
16
17
|
pluginName: "navigation",
|
|
17
18
|
},
|
|
18
19
|
{
|
|
19
20
|
section: "plugins",
|
|
20
|
-
displayName: "
|
|
21
|
-
uid:
|
|
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')
|
|
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,
|