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
package/README.md CHANGED
@@ -1,22 +1,35 @@
1
+ <div align="center" width="150px">
2
+ <img style="width: 150px; height: auto;" src="public/assets/logo.png" alt="Logo - Strapi Navigation plugin" />
3
+ </div>
1
4
  <div align="center">
2
- <h1>Strapi v4 - Navigation plugin - BETA</h1>
3
- <p>Create consumable navigation with a simple and straigthforward visual builder.</p>
5
+ <h1>Strapi v4 - Navigation plugin</h1>
6
+ <p>Create consumable navigation with a simple and straightforward visual builder</p>
4
7
  <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" />
8
+ <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
9
  </a>
7
10
  <a href="https://www.npmjs.org/package/strapi-plugin-navigation">
8
11
  <img src="https://img.shields.io/npm/dm/strapi-plugin-navigation.svg" alt="Monthly download on NPM" />
9
12
  </a>
10
13
  <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" />
14
+ <img src="https://circleci.com/gh/VirtusLab-Open-Source/strapi-plugin-navigation.svg?style=shield" alt="CircleCI" />
12
15
  </a>
13
16
  <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" />
17
+ <img src="https://codecov.io/gh/VirtusLab/strapi-plugin-navigation/coverage.svg?branch=master" alt="codecov.io" />
18
+ </a>
19
+ <a href="https://sharing.clickup.com/tl/xhcmx-43/strapiv-4-navigation-roadmap">
20
+ <img src="https://img.shields.io/website?down_message=roadmap&label=product&up_message=roadmap&url=https%3A%2F%2Fsharing.clickup.com%2Ftl%2Fxhcmx-43%2Fstrapiv-4-navigation-roadmap" />
21
+ </a>
22
+ <a href="https://sharing.clickup.com/b/6-169004201-2/strapiv-4-navigation-board">
23
+ <img src="https://img.shields.io/website?down_message=board&label=product&up_color=blue&up_message=board&url=https%3A%2F%2Fsharing.clickup.com%2Fb%2F6-169004201-2%2Fstrapiv-4-navigation-board" />
15
24
  </a>
16
25
  </div>
17
26
 
18
27
  ---
19
28
 
29
+ <div style="margin: 20px 0" align="center">
30
+ <img style="width: 100%; height: auto;" src="public/assets/preview.png" alt="UI preview" />
31
+ </div>
32
+
20
33
  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
34
 
22
35
  - Flat
@@ -28,14 +41,14 @@ Strapi Navigation Plugin provides a website navigation / menu builder feature fo
28
41
  - **Navigation Public API:** Simple and ready for use API endpoint for consuming the navigation structure you've created
29
42
  - **Visual builder:** Elegant and easy to use visual builder
30
43
  - **Any Content Type relation:** Navigation can by linked to any of your Content Types by default. Simply, you're controlling it and also limiting available content types by configuration props
44
+ - **Multiple navigations:** Create as many Navigation containers as you want, setup them and use in the consumer application
31
45
  - **Customizable:** Possibility to customize the options like: available Content Types, Maximum level for "attach to menu", Additional fields (audience)
32
46
  - **[Audit log](https://github.com/VirtusLab/strapi-molecules/tree/master/packages/strapi-plugin-audit-log):** integration with Strapi Molecules Audit Log plugin that provides changes track record
33
47
 
34
-
35
48
  ## ⚙️ Versions
36
49
 
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)
50
+ - **Strapi v4** - (current) - [v2.x](https://github.com/VirtusLab-Open-Source/strapi-plugin-navigation)
51
+ - **Strapi v3** - [v1.x](https://github.com/VirtusLab-Open-Source/strapi-plugin-navigation/tree/strapi-v3)
39
52
 
40
53
  ## ⏳ Installation
41
54
 
@@ -68,12 +81,10 @@ Complete installation requirements are exact same as for Strapi itself and can b
68
81
 
69
82
  **Supported Strapi versions**:
70
83
 
71
- - Strapi v4.0.5 (recently tested)
84
+ - Strapi v4.0.7 (recently tested)
72
85
  - Strapi v4.x
73
86
 
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.
87
+ > 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
88
 
78
89
  **We recommend always using the latest version of Strapi to start your new projects**.
79
90
 
@@ -105,7 +116,7 @@ Config for this plugin is stored as a part of `config/plugins.js` or `config/<en
105
116
  - `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
117
  - `gql` - If you're using GraphQL that's the right place to put all necessary settings. More **[ here ](#gql-configuration)**
107
118
 
108
- ## GQL Configuration
119
+ ## 🔧 GQL Configuration
109
120
  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
121
 
111
122
  ```gql
@@ -131,8 +142,14 @@ gql: {
131
142
  ```
132
143
  where `Page` and `UploadFile` are your type names for the **Content Types** you're referring by navigation items relations.
133
144
 
145
+ ## 👤 RBAC
146
+ Plugin provides granular permissions based on Strapi RBAC functionality.
147
+
148
+ ### Mandatory permissions
149
+ For any role different than **Super Admin**, to access the **Navigation panel** you must set following permissions:
150
+ - _Plugins_ -> _Navigation_ -> _Read_ - gives you the access to **Navigation Panel**
134
151
 
135
- ## Public API Navigation Item model
152
+ ## Base Navigation Item model
136
153
 
137
154
  ### Flat
138
155
  ```
@@ -146,8 +163,8 @@ where `Page` and `UploadFile` are your type names for the **Content Types** you'
146
163
  "menuAttached": false,
147
164
  "parent": 8, // Parent Navigation Item 'id', null in case of root level
148
165
  "master": 1, // Navigation 'id'
149
- "created_at": "2020-09-29T13:29:19.086Z",
150
- "updated_at": "2020-09-29T13:29:19.128Z",
166
+ "createdAt": "2020-09-29T13:29:19.086Z",
167
+ "updatedAt": "2020-09-29T13:29:19.128Z",
151
168
  "related": [ <Content Type model > ],
152
169
  "audience": []
153
170
  }
@@ -198,7 +215,7 @@ where `Page` and `UploadFile` are your type names for the **Content Types** you'
198
215
  }
199
216
  ```
200
217
 
201
- ## Public API specification
218
+ ## 🕸️ Public API specification
202
219
 
203
220
  ### Render
204
221
 
@@ -378,16 +395,26 @@ Live example of plugin usage can be found in the [VirtusLab Strapi Examples](htt
378
395
 
379
396
  ## 🤝 Contributing
380
397
 
398
+ <div>
399
+ <a href="https://sharing.clickup.com/tl/xhcmx-43/strapiv-4-navigation-roadmap">
400
+ <img src="https://img.shields.io/website?down_message=roadmap&label=product&up_message=roadmap&url=https%3A%2F%2Fsharing.clickup.com%2Ftl%2Fxhcmx-43%2Fstrapiv-4-navigation-roadmap" />
401
+ </a>
402
+ <a href="https://sharing.clickup.com/b/6-169004201-2/strapiv-4-navigation-board">
403
+ <img src="https://img.shields.io/website?down_message=board&label=product&up_color=blue&up_message=board&url=https%3A%2F%2Fsharing.clickup.com%2Fb%2F6-169004201-2%2Fstrapiv-4-navigation-board" />
404
+ </a>
405
+ </div>
406
+
381
407
  Feel free to fork and make a Pull Request to this plugin project. All the input is warmly welcome!
382
408
 
383
409
  ## 👨‍💻 Community support
384
410
 
385
411
  For general help using Strapi, please refer to [the official Strapi documentation](https://strapi.io/documentation/). For additional help, you can use one of these channels to ask a question:
386
412
 
387
- - [Slack](http://slack.strapi.io) We're present on official Strapi slack workspace. Look for @cyp3r and DM.
413
+ - [Discord](https://discord.strapi.io/) We're present on official Strapi Discord workspace. Find us by `[VirtusLab]` prefix and DM.
388
414
  - [Slack - VirtusLab Open Source](https://virtuslab-oss.slack.com) We're present on a public channel #strapi-molecules
389
415
  - [GitHub](https://github.com/VirtusLab/strapi-plugin-navigation/issues) (Bug reports, Contributions, Questions and Discussions)
416
+ - [E-mail](mailto:strapi@virtuslab.com) - we will respond back as soon as possible
390
417
 
391
418
  ## 📝 License
392
419
 
393
- [MIT License](LICENSE.md) Copyright (c) 2021 [VirtusLab Sp. z o.o.](https://virtuslab.com/) &amp; [Strapi Solutions](https://strapi.io/).
420
+ [MIT License](LICENSE.md) Copyright (c) [VirtusLab Sp. z o.o.](https://virtuslab.com/) &amp; [Strapi Solutions](https://strapi.io/).
@@ -0,0 +1,25 @@
1
+ {
2
+ "kind": "collectionType",
3
+ "collectionName": "pages",
4
+ "info": {
5
+ "singularName": "page",
6
+ "pluralName": "pages",
7
+ "displayName": "Page",
8
+ "name": "page"
9
+ },
10
+ "options": {
11
+ "increments": true,
12
+ "timestamps": true,
13
+ "searchable": true,
14
+ "previewable": true,
15
+ "draftAndPublish": false
16
+ },
17
+ "pluginOptions": {},
18
+ "attributes": {
19
+ "title": {
20
+ "type": "string",
21
+ "required": true
22
+ }
23
+ }
24
+ }
25
+
@@ -0,0 +1,196 @@
1
+ const { isMatch } = require('lodash');
2
+
3
+ const masterModelMock = {
4
+ findOne: () => ({
5
+ id: 1,
6
+ name: "Main navigation",
7
+ slug: "main-navigation",
8
+ visible: true,
9
+ createdAt: "2021-12-30T14:05:50.276Z",
10
+ updatedAt: "2021-12-30T14:05:50.276Z",
11
+ }),
12
+ findMany: () => [{
13
+ id: 1,
14
+ name: "Main navigation",
15
+ slug: "main-navigation",
16
+ visible: true,
17
+ createdAt: "2021-12-30T14:05:50.276Z",
18
+ updatedAt: "2021-12-30T14:05:50.276Z",
19
+ }],
20
+ };
21
+
22
+ const itemModelMock = {
23
+ findOne: async () => ({
24
+ id: 1,
25
+ title: "home",
26
+ type: "INTERNAL",
27
+ path: "home1",
28
+ externalPath: null,
29
+ uiRouterKey: "home",
30
+ menuAttached: true,
31
+ order: 1,
32
+ createdAt: "2021-12-31T10:04:54.812Z",
33
+ updatedAt: "2022-01-14T13:36:29.430Z",
34
+ related: {
35
+ id: 1,
36
+ related_id: "1",
37
+ related_type: "api::pages.pages",
38
+ field: "navigation",
39
+ order: 1,
40
+ master: "3",
41
+ createdAt: "2021-12-31T10:04:54.800Z",
42
+ updatedAt: "2021-12-31T10:04:54.800Z",
43
+ navigationItemId: 56,
44
+ },
45
+ parent: null,
46
+ }),
47
+ findMany: async ({where}) => [{
48
+ id: 1,
49
+ title: "home",
50
+ type: "INTERNAL",
51
+ path: "home1",
52
+ externalPath: null,
53
+ uiRouterKey: "home",
54
+ menuAttached: true,
55
+ order: 1,
56
+ createdAt: "2021-12-31T10:04:54.812Z",
57
+ updatedAt: "2022-01-14T13:36:29.430Z",
58
+ master: 1,
59
+ related: {
60
+ id: 1,
61
+ related_id: "1",
62
+ related_type: "api::pages.pages",
63
+ field: "navigation",
64
+ order: 1,
65
+ master: "3",
66
+ createdAt: "2021-12-31T10:04:54.800Z",
67
+ updatedAt: "2021-12-31T10:04:54.800Z",
68
+ navigationItemId: 56,
69
+ },
70
+ parent: null,
71
+ }, {
72
+ id: 2,
73
+ title: "side",
74
+ type: "INTERNAL",
75
+ path: "side",
76
+ externalPath: null,
77
+ uiRouterKey: "side",
78
+ menuAttached: false,
79
+ order: 1,
80
+ createdAt: "2021-12-31T10:04:54.824Z",
81
+ updatedAt: "2021-12-31T12:47:20.508Z",
82
+ master: 1,
83
+ related: {
84
+ id: 2,
85
+ related_id: "2",
86
+ related_type: "api::pages.pages",
87
+ field: "navigation",
88
+ order: 1,
89
+ master: "3",
90
+ createdAt: "2021-12-31T10:04:54.823Z",
91
+ updatedAt: "2021-12-31T10:04:54.823Z",
92
+ navigationItemId: 57,
93
+ },
94
+ parent: {
95
+ id: 1,
96
+ title: "home",
97
+ type: "INTERNAL",
98
+ path: "home1",
99
+ externalPath: null,
100
+ uiRouterKey: "home",
101
+ menuAttached: true,
102
+ order: 1,
103
+ createdAt: "2021-12-31T10:04:54.812Z",
104
+ updatedAt: "2022-01-14T13:36:29.430Z",
105
+ },
106
+ }].filter(item => isMatch(item, where)),
107
+ };
108
+
109
+ const pageModelMock = {
110
+ findOne: async ({ where }) => ({
111
+ "id": 1,
112
+ "attributes": {
113
+ "title": "Page nr 1",
114
+ "createdAt": "2022-01-19T08:22:31.244Z",
115
+ "updatedAt": "2022-01-19T08:22:31.244Z",
116
+ "publishedAt": null
117
+ }
118
+ }),
119
+ findMany: async ({ where }) => [{
120
+ "id": 1,
121
+ "attributes": {
122
+ "title": "Page nr 1",
123
+ "createdAt": "2022-01-19T08:22:31.244Z",
124
+ "updatedAt": "2022-01-19T08:22:31.244Z",
125
+ "publishedAt": null
126
+ }
127
+ }, {
128
+ "id": 2,
129
+ "attributes": {
130
+ "title": "Page nr 2",
131
+ "createdAt": "2022-01-19T08:22:50.821Z",
132
+ "updatedAt": "2022-01-19T08:22:50.821Z",
133
+ "publishedAt": null
134
+ }
135
+ }]
136
+
137
+ };
138
+
139
+ const plugins = (strapi) => ({
140
+ navigation: {
141
+ get services() { return require('../server/services') },
142
+ service: (key) => (require('../server/services'))[key]({ strapi }),
143
+ get contentTypes() { return require('../server/content-types') },
144
+ contentType: (key) => preparePluginContentType(require('../server/content-types')[key].schema, 'navigation'),
145
+ config: (key) => ({
146
+ ...require('../server/config').default,
147
+ contentTypes: ['api::pages.pages'],
148
+ })[key],
149
+ }
150
+ });
151
+
152
+ const contentTypes = {
153
+ 'api::pages.pages': {
154
+ ...require('./pages.settings.json'),
155
+ uid: 'api::pages.pages',
156
+ modelName: 'page',
157
+ },
158
+ };
159
+
160
+ const preparePluginContentType = (schema, plugin) => {
161
+ const { name } = schema.info;
162
+
163
+ return {
164
+ ...schema,
165
+ uid: `plugin::${plugin}.${name}`,
166
+ modelName: name,
167
+ }
168
+ }
169
+
170
+ const strapiFactory = (plugins, contentTypes) => ({
171
+ get plugins() { return plugins(strapi) },
172
+ plugin: (name) => plugins(strapi)[name],
173
+ get contentTypes() { return contentTypes },
174
+ contentType: (key) => contentTypes[key],
175
+ query: (model) => {
176
+ switch (model) {
177
+ case 'plugin::navigation.navigation':
178
+ return masterModelMock;
179
+ case 'plugin::navigation.navigation-item':
180
+ return itemModelMock;
181
+ case 'api::pages.pages':
182
+ return pageModelMock;
183
+ default:
184
+ return {
185
+ findOne: () => ({}),
186
+ findMany: () => [],
187
+ }
188
+ }
189
+ }
190
+ });
191
+
192
+ const setupStrapi = () => {
193
+ Object.defineProperty(global, 'strapi', { value: strapiFactory(plugins, contentTypes) });
194
+ }
195
+
196
+ module.exports = { setupStrapi };
@@ -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,21 @@ 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: ${props.theme.spaces[1]};
9
+ margin: 0px ${props.theme.spaces[3]};
10
+ vertical-align: middle;
11
+
12
+ cursor: default;
13
+
14
+ span {
15
+ font-size: .55rem;
16
+ line-height: 1;
17
+ vertical-align: middle;
18
+ }
19
+ `}
6
20
  `;
7
21
 
8
22
  export default ItemCardBadge;
@@ -4,6 +4,7 @@ import { useIntl } from 'react-intl';
4
4
  import { Flex } from '@strapi/design-system/Flex';
5
5
  import { IconButton } from '@strapi/design-system/IconButton';
6
6
  import { Typography } from '@strapi/design-system/Typography';
7
+ import { Icon } from '@strapi/design-system/Icon';
7
8
  import PencilIcon from '@strapi/icons/Pencil';
8
9
  import TrashIcon from '@strapi/icons/Trash';
9
10
  import RefreshIcon from '@strapi/icons/Refresh';
@@ -12,14 +13,14 @@ import Wrapper from './Wrapper';
12
13
  import ItemCardBadge from '../ItemCardBadge';
13
14
  import { getTrad } from "../../../translations";
14
15
 
15
- const ItemCardHeader = ({ title, path, icon, removed, isExternal, isPublished, onItemRemove, onItemEdit, onItemRestore }) => {
16
- const badgeColor = isPublished ? 'success' : 'secondary';
16
+ const ItemCardHeader = ({ title, path, icon, removed, onItemRemove, onItemEdit, onItemRestore }) => {
17
+
17
18
  const { formatMessage } = useIntl();
18
19
 
19
20
  return (
20
21
  <Wrapper>
21
22
  <Flex alignItems="center">
22
- {icon}
23
+ <Icon as={icon} />
23
24
  <Typography variant="omega" fontWeight="bold">
24
25
  {title}
25
26
  </Typography>
@@ -27,23 +28,15 @@ const ItemCardHeader = ({ title, path, icon, removed, isExternal, isPublished, o
27
28
  {path}
28
29
  </Typography>
29
30
  </Flex>
30
- <Flex alignItems="center">
31
- {removed ?
32
- <ItemCardBadge
31
+ <Flex alignItems="center" style={{ zIndex: 2 }}>
32
+ {removed &&
33
+ (<ItemCardBadge
33
34
  borderColor={`danger200`}
34
35
  backgroundColor={`danger100`}
35
36
  textColor={`danger600`}
36
37
  >
37
38
  {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>
39
+ </ItemCardBadge>)
47
40
  }
48
41
 
49
42
  <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,69 @@ 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
+ borderColor={`${relatedBadgeColor}200`}
106
+ backgroundColor={`${relatedBadgeColor}100`}
107
+ textColor={`${relatedBadgeColor}600`}
108
+ className="action"
109
+ small
110
+ >
111
+ {formatMessage(getTrad(`navigation.item.badge.${isPublished ? 'published' : 'draft'}`), {
112
+ type: relatedTypeLabel
113
+ })}
114
+ </ItemCardBadge>
115
+ <Typography variant="pi" fontWeight="bold" textColor="neutral600">
116
+ { relatedItemLabel }
117
+ <Link
118
+ to={`/content-manager/collectionType/${relatedRef?.__collectionUid}/${relatedRef?.id}`}
119
+ endIcon={<ArrowRight />}>&nbsp;</Link>
120
+ </Typography>
121
+ </Box>)
122
+ }
123
+ </Flex>
124
+ </CardBody>)}
87
125
  </Card>
88
126
  {hasChildren && !removed && <List
89
127
  onItemLevelAdd={onItemLevelAdd}
@@ -92,10 +130,12 @@ const Item = (props) => {
92
130
  onItemRestore={onItemRestore}
93
131
  error={error}
94
132
  allowedLevels={allowedLevels}
95
- isParentAttachedToMenu={true}
133
+ isParentAttachedToMenu={menuAttached}
96
134
  items={item.items}
97
135
  level={level + 1}
98
136
  levelPath={absolutePath}
137
+ contentTypes={contentTypes}
138
+ contentTypesNameFields={contentTypesNameFields}
99
139
  />
100
140
  }
101
141
  </Wrapper>
@@ -110,7 +150,6 @@ Item.propTypes = {
110
150
  uiRouterKey: PropTypes.string,
111
151
  path: PropTypes.string,
112
152
  externalPath: PropTypes.string,
113
- audience: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])),
114
153
  related: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
115
154
  menuAttached: PropTypes.bool
116
155
  }).isRequired,
@@ -121,6 +160,10 @@ Item.propTypes = {
121
160
  onItemRestore: PropTypes.func.isRequired,
122
161
  onItemLevelAdd: PropTypes.func.isRequired,
123
162
  onItemRemove: PropTypes.func.isRequired,
163
+ config: PropTypes.shape({
164
+ contentTypes: PropTypes.array.isRequired,
165
+ contentTypesNameFields: PropTypes.object.isRequired,
166
+ }).isRequired
124
167
  };
125
168
 
126
169
  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;