strapi-plugin-navigation 2.0.0-beta.3 → 2.0.0-beta.4
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 +62 -104
- package/admin/src/components/Item/ItemCardHeader/index.js +2 -2
- package/admin/src/components/Item/index.js +3 -1
- package/admin/src/components/NavigationItemList/index.js +2 -0
- package/admin/src/components/Search/index.js +49 -0
- package/admin/src/pages/View/index.js +16 -4
- package/admin/src/pages/View/utils/parsers.js +1 -1
- package/package.json +5 -2
- package/server/config/index.js +8 -0
- package/server/controllers/navigation.js +21 -0
- package/server/graphql/index.js +23 -0
- package/server/graphql/queries/index.js +17 -0
- package/server/graphql/queries/render-navigation-child.js +16 -0
- package/server/graphql/queries/render-navigation.js +15 -0
- package/server/graphql/resolvers-config.js +4 -0
- package/server/graphql/types/content-types-name-fields.js +8 -0
- package/server/graphql/types/content-types.js +16 -0
- package/server/graphql/types/create-navigation-item.js +17 -0
- package/server/graphql/types/create-navigation-related.js +8 -0
- package/server/graphql/types/create-navigation.js +7 -0
- package/server/graphql/types/index.js +15 -0
- package/server/graphql/types/navigation-config.js +9 -0
- package/server/graphql/types/navigation-details.js +10 -0
- package/server/graphql/types/navigation-item.js +29 -0
- package/server/graphql/types/navigation-related.js +23 -0
- package/server/graphql/types/navigation-render-type.js +4 -0
- package/server/graphql/types/navigation.js +9 -0
- package/server/register.js +5 -0
- package/server/routes/client.js +21 -0
- package/server/routes/index.js +2 -1
- package/server/services/navigation.js +272 -6
- package/server/services/utils/functions.js +84 -2
- package/strapi-server.js +3 -1
package/README.md
CHANGED
|
@@ -1,34 +1,45 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
<p
|
|
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>
|
|
4
4
|
<a href="https://www.npmjs.org/package/strapi-plugin-navigation">
|
|
5
|
-
<img src="https://img.shields.io/
|
|
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
6
|
</a>
|
|
7
7
|
<a href="https://www.npmjs.org/package/strapi-plugin-navigation">
|
|
8
8
|
<img src="https://img.shields.io/npm/dm/strapi-plugin-navigation.svg" alt="Monthly download on NPM" />
|
|
9
9
|
</a>
|
|
10
10
|
<a href="https://circleci.com/gh/VirtusLab/strapi-plugin-navigation">
|
|
11
|
-
<img src="https://circleci.com/gh/VirtusLab/strapi-plugin-navigation.svg?style=shield" alt="CircleCI" />
|
|
11
|
+
<img src="https://circleci.com/gh/VirtusLab-Open-Source/strapi-plugin-navigation/tree/feat%2Fstrapi-v4-support.svg?style=shield" alt="CircleCI" />
|
|
12
12
|
</a>
|
|
13
13
|
<a href="https://codecov.io/gh/VirtusLab/strapi-plugin-navigation">
|
|
14
|
-
<img src="https://codecov.io/gh/VirtusLab/strapi-plugin-navigation/coverage.svg?branch=
|
|
14
|
+
<img src="https://codecov.io/gh/VirtusLab/strapi-plugin-navigation/coverage.svg?branch=feat%2Fstrapi-v4-support" alt="codecov.io" />
|
|
15
15
|
</a>
|
|
16
|
-
</
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
---
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
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:
|
|
19
21
|
|
|
20
22
|
- Flat
|
|
21
23
|
- Tree (nested)
|
|
22
24
|
- RFR (ready for handling by Redux First Router)
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
## ✨ Features
|
|
27
|
+
|
|
28
|
+
- **Navigation Public API:** Simple and ready for use API endpoint for consuming the navigation structure you've created
|
|
29
|
+
- **Visual builder:** Elegant and easy to use visual builder
|
|
30
|
+
- **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
|
|
31
|
+
- **Customizable:** Possibility to customize the options like: available Content Types, Maximum level for "attach to menu", Additional fields (audience)
|
|
32
|
+
- **[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
|
+
|
|
34
|
+
|
|
35
|
+
## ⚙️ Versions
|
|
25
36
|
|
|
26
37
|
- **Stable** - [v1.1.2](https://github.com/VirtusLab-Open-Source/strapi-plugin-navigation)
|
|
27
38
|
- **Beta** - v4 support - [v2.0.0-beta.x](https://github.com/VirtusLab-Open-Source/strapi-plugin-navigation/tree/feat/strapi-v4-support)
|
|
28
39
|
|
|
29
|
-
|
|
40
|
+
## ⏳ Installation
|
|
30
41
|
|
|
31
|
-
|
|
42
|
+
It's recommended to use **yarn** to install this plugin within your Strapi project. [You can install yarn with these docs](https://yarnpkg.com/lang/en/docs/install/).
|
|
32
43
|
|
|
33
44
|
```bash
|
|
34
45
|
yarn add strapi-plugin-navigation@latest
|
|
@@ -49,87 +60,53 @@ yarn develop --watch-admin
|
|
|
49
60
|
|
|
50
61
|
The **UI Navigation** plugin should appear in the **Plugins** section of Strapi sidebar after you run app again.
|
|
51
62
|
|
|
52
|
-
|
|
53
63
|
Enjoy 🎉
|
|
54
64
|
|
|
55
|
-
|
|
65
|
+
## 🖐 Requirements
|
|
56
66
|
|
|
57
|
-
Complete installation requirements are exact same as for Strapi itself and can be found in the documentation under <a href="https://strapi.io/
|
|
67
|
+
Complete installation requirements are exact same as for Strapi itself and can be found in the documentation under <a href="https://docs.strapi.io/developer-docs/latest/setup-deployment-guides/installation/cli.html#preparing-the-installation">Installation Requirements</a>.
|
|
58
68
|
|
|
59
69
|
**Supported Strapi versions**:
|
|
60
70
|
|
|
61
|
-
- Strapi v4.0.
|
|
62
|
-
|
|
63
|
-
(This plugin is not working with v3.x and not may work with the older Strapi v4 versions, but these are not tested nor officially supported at this time.)
|
|
64
|
-
|
|
65
|
-
**We recommend always using the latest version of Strapi to start your new projects**.
|
|
66
|
-
|
|
67
|
-
## Features
|
|
68
|
-
|
|
69
|
-
- **Navigation Public API:** Simple and ready for use API endpoint for getting the navigation structure you've created
|
|
70
|
-
- **Visual builder:** Elegant and easy to use visual builder
|
|
71
|
-
- **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
|
|
72
|
-
- **Customizable:** Possibility to customize the options like: available Content Types, Maximum level for "attach to menu", Additional fields (audience)
|
|
73
|
-
- **[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
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
## Content Type model relation to Navigation Item
|
|
77
|
-
|
|
78
|
-
To enable Content Type to work with Navigation Item, you've to add following field to your model `*.settings.json`:
|
|
71
|
+
- Strapi v4.0.5 (recently tested)
|
|
72
|
+
- Strapi v4.x
|
|
79
73
|
|
|
80
|
-
|
|
81
|
-
"navigation": {
|
|
82
|
-
"model": "navigationitem",
|
|
83
|
-
"plugin": "navigation",
|
|
84
|
-
"via": "related",
|
|
85
|
-
"configurable": false,
|
|
86
|
-
"hidden": true
|
|
87
|
-
}
|
|
88
|
-
```
|
|
74
|
+
_This plugin is not working with v3.x._
|
|
89
75
|
|
|
90
|
-
|
|
76
|
+
It may or may not work with the older Strapi v4 versions, these are not tested nor officially supported at this time.
|
|
91
77
|
|
|
92
|
-
|
|
93
|
-
"attributes": {
|
|
94
|
-
...,
|
|
95
|
-
"navigation": {
|
|
96
|
-
"model": "navigationitem",
|
|
97
|
-
"plugin": "navigation",
|
|
98
|
-
"via": "related",
|
|
99
|
-
"configurable": false,
|
|
100
|
-
"hidden": true
|
|
101
|
-
},
|
|
102
|
-
...
|
|
103
|
-
},
|
|
104
|
-
```
|
|
78
|
+
**We recommend always using the latest version of Strapi to start your new projects**.
|
|
105
79
|
|
|
106
|
-
## Configuration
|
|
107
|
-
|
|
80
|
+
## 🔧 Configuration
|
|
81
|
+
Config for this plugin is stored as a part of `config/plugins.js` or `config/<env>/plugins.js` file. You can use following snippet to make sure that the config structure is correct. If you've got already configurations for other plugins stores by this way, you can use the `navigation` along with them.
|
|
108
82
|
|
|
109
83
|
```js
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
84
|
+
module.exports = ({ env }) => ({
|
|
85
|
+
// ...
|
|
86
|
+
navigation: {
|
|
87
|
+
enabled: true,
|
|
88
|
+
config: {
|
|
89
|
+
additionalFields: ['audience'],
|
|
90
|
+
contentTypes: ['api::page.page'],
|
|
91
|
+
contentTypesNameFields: {
|
|
92
|
+
'api::page.page': ['title']
|
|
93
|
+
},
|
|
94
|
+
allowedLevels: 2,
|
|
95
|
+
gql: {...},
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
});
|
|
123
99
|
```
|
|
124
100
|
|
|
125
101
|
### Properties
|
|
126
102
|
- `additionalFields` - Additional fields: 'audience', more in the future
|
|
127
103
|
- `allowedLevels` - Maximum level for which your're able to mark item as "Menu attached"
|
|
128
|
-
- `
|
|
104
|
+
- `contentTypes` - UIDs of related content types
|
|
105
|
+
- `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`
|
|
129
106
|
- `gql` - If you're using GraphQL that's the right place to put all necessary settings. More **[ here ](#gql-configuration)**
|
|
130
107
|
|
|
131
108
|
## GQL Configuration
|
|
132
|
-
To properly configure GQL to work with navigation you should provide `gql` prop
|
|
109
|
+
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:
|
|
133
110
|
|
|
134
111
|
```gql
|
|
135
112
|
master: Int
|
|
@@ -137,11 +114,11 @@ items: [NavigationItem]
|
|
|
137
114
|
related: NavigationRelated
|
|
138
115
|
```
|
|
139
116
|
|
|
140
|
-
as follows:
|
|
117
|
+
This prop should look as follows:
|
|
141
118
|
|
|
142
119
|
```js
|
|
143
120
|
gql: {
|
|
144
|
-
navigationItemRelated: '
|
|
121
|
+
navigationItemRelated: ['<your GQL related content types>'],
|
|
145
122
|
},
|
|
146
123
|
```
|
|
147
124
|
|
|
@@ -149,10 +126,10 @@ for example:
|
|
|
149
126
|
|
|
150
127
|
```js
|
|
151
128
|
gql: {
|
|
152
|
-
|
|
129
|
+
navigationItemRelated: ['Page', 'UploadFile'],
|
|
153
130
|
},
|
|
154
131
|
```
|
|
155
|
-
where `
|
|
132
|
+
where `Page` and `UploadFile` are your type names for the **Content Types** you're referring by navigation items relations.
|
|
156
133
|
|
|
157
134
|
|
|
158
135
|
## Public API Navigation Item model
|
|
@@ -225,13 +202,13 @@ where `Pages` and `UploadFile` are your types to the **Content Types** you're re
|
|
|
225
202
|
|
|
226
203
|
### Render
|
|
227
204
|
|
|
228
|
-
`GET <host>/navigation/render/<idOrSlug>?type=<type>`
|
|
205
|
+
`GET <host>/api/navigation/render/<idOrSlug>?type=<type>`
|
|
229
206
|
|
|
230
207
|
Return a rendered navigation structure depends on passed type (`tree`, `rfr` or nothing to render as `flat/raw`).
|
|
231
208
|
|
|
232
209
|
*Note: The ID of navigation by default is `1`, that's for future extensions and multi-navigation feature.*
|
|
233
210
|
|
|
234
|
-
**Example URL**: `https://localhost:1337/navigation/render/1`
|
|
211
|
+
**Example URL**: `https://localhost:1337/api/navigation/render/1`
|
|
235
212
|
|
|
236
213
|
**Example response body**
|
|
237
214
|
|
|
@@ -260,7 +237,7 @@ Return a rendered navigation structure depends on passed type (`tree`, `rfr` or
|
|
|
260
237
|
]
|
|
261
238
|
```
|
|
262
239
|
|
|
263
|
-
**Example URL**: `https://localhost:1337/navigation/render/1?type=tree`
|
|
240
|
+
**Example URL**: `https://localhost:1337/api/navigation/render/1?type=tree`
|
|
264
241
|
|
|
265
242
|
**Example response body**
|
|
266
243
|
|
|
@@ -296,7 +273,7 @@ Return a rendered navigation structure depends on passed type (`tree`, `rfr` or
|
|
|
296
273
|
]
|
|
297
274
|
```
|
|
298
275
|
|
|
299
|
-
**Example URL**: `https://localhost:1337/navigation/render/1?type=rfr`
|
|
276
|
+
**Example URL**: `https://localhost:1337/api/navigation/render/1?type=rfr`
|
|
300
277
|
|
|
301
278
|
**Example response body**
|
|
302
279
|
|
|
@@ -387,30 +364,11 @@ For collection types it will be read from content type's attribute name `templat
|
|
|
387
364
|
|
|
388
365
|
For single types a global name of this content type will be used as a template name or it can be set manually with an option named `templateName`.
|
|
389
366
|
|
|
390
|
-
##
|
|
391
|
-
If you would like to use the [Strapi Molecules Audit Log](https://github.com/VirtusLab/strapi-molecules/tree/master/packages/strapi-plugin-audit-log) plugin you've to first install and then add in you `config/middleware.js` following section enable it:
|
|
392
|
-
```js
|
|
393
|
-
{
|
|
394
|
-
'audit-log': {
|
|
395
|
-
enabled: true,
|
|
396
|
-
exclude: [],
|
|
397
|
-
map: [
|
|
398
|
-
{
|
|
399
|
-
pluginName: 'navigation',
|
|
400
|
-
serviceName: 'navigation',
|
|
401
|
-
Class: Navigation,
|
|
402
|
-
},
|
|
403
|
-
]
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
```
|
|
407
|
-
As a last step you've to provide the Navigation class to let Audit Log use it. To not provide external & hard dependencies we've added the example of class code in the `examples/audit-log-integration.js` .
|
|
408
|
-
|
|
409
|
-
## Examples
|
|
367
|
+
## 🧩 Examples
|
|
410
368
|
|
|
411
369
|
Live example of plugin usage can be found in the [VirtusLab Strapi Examples](https://github.com/VirtusLab/strapi-examples/tree/master/strapi-plugin-navigation) repository.
|
|
412
370
|
|
|
413
|
-
## Q&A
|
|
371
|
+
## 💬 Q&A
|
|
414
372
|
|
|
415
373
|
### Content Types
|
|
416
374
|
|
|
@@ -418,11 +376,11 @@ Live example of plugin usage can be found in the [VirtusLab Strapi Examples](htt
|
|
|
418
376
|
|
|
419
377
|
**A:** As an authors of the plugin we're not supporting any editing of mentioned content types via built-in Strapi Content Manager. Plugin delivers highly customized & extended functionality which might be covered only by dedicated editor UI accessible via **Plugins Section > UI Navigation**. Only issues that has been recognized there, are in the scope of support we've providing.
|
|
420
378
|
|
|
421
|
-
## Contributing
|
|
379
|
+
## 🤝 Contributing
|
|
422
380
|
|
|
423
381
|
Feel free to fork and make a Pull Request to this plugin project. All the input is warmly welcome!
|
|
424
382
|
|
|
425
|
-
## Community support
|
|
383
|
+
## 👨💻 Community support
|
|
426
384
|
|
|
427
385
|
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:
|
|
428
386
|
|
|
@@ -430,6 +388,6 @@ For general help using Strapi, please refer to [the official Strapi documentatio
|
|
|
430
388
|
- [Slack - VirtusLab Open Source](https://virtuslab-oss.slack.com) We're present on a public channel #strapi-molecules
|
|
431
389
|
- [GitHub](https://github.com/VirtusLab/strapi-plugin-navigation/issues) (Bug reports, Contributions, Questions and Discussions)
|
|
432
390
|
|
|
433
|
-
## License
|
|
391
|
+
## 📝 License
|
|
434
392
|
|
|
435
393
|
[MIT License](LICENSE.md) Copyright (c) 2021 [VirtusLab Sp. z o.o.](https://virtuslab.com/) & [Strapi Solutions](https://strapi.io/).
|
|
@@ -12,7 +12,7 @@ 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, isPublished, onItemRemove, onItemEdit, onItemRestore }) => {
|
|
15
|
+
const ItemCardHeader = ({ title, path, icon, removed, isExternal, isPublished, onItemRemove, onItemEdit, onItemRestore }) => {
|
|
16
16
|
const badgeColor = isPublished ? 'success' : 'secondary';
|
|
17
17
|
const { formatMessage } = useIntl();
|
|
18
18
|
|
|
@@ -36,7 +36,7 @@ const ItemCardHeader = ({ title, path, icon, removed, isPublished, onItemRemove,
|
|
|
36
36
|
>
|
|
37
37
|
{formatMessage(getTrad("navigation.item.badge.removed"))}
|
|
38
38
|
</ItemCardBadge>
|
|
39
|
-
: <ItemCardBadge
|
|
39
|
+
: !isExternal && <ItemCardBadge
|
|
40
40
|
borderColor={`${badgeColor}200`}
|
|
41
41
|
backgroundColor={`${badgeColor}100`}
|
|
42
42
|
textColor={`${badgeColor}600`}
|
|
@@ -31,6 +31,7 @@ const Item = (props) => {
|
|
|
31
31
|
onItemRestore,
|
|
32
32
|
onItemEdit,
|
|
33
33
|
error,
|
|
34
|
+
displayChildren,
|
|
34
35
|
} = props;
|
|
35
36
|
|
|
36
37
|
const {
|
|
@@ -48,7 +49,7 @@ const Item = (props) => {
|
|
|
48
49
|
const isPublished = relatedRef && relatedRef?.publishedAt;
|
|
49
50
|
const isNextMenuAllowedLevel = isNumber(allowedLevels) ? level < (allowedLevels - 1) : true;
|
|
50
51
|
const isMenuAllowedLevel = isNumber(allowedLevels) ? level < allowedLevels : true;
|
|
51
|
-
const hasChildren = !isEmpty(item.items) && !isExternal;
|
|
52
|
+
const hasChildren = !isEmpty(item.items) && !isExternal && !displayChildren;
|
|
52
53
|
const absolutePath = isExternal ? undefined : `${levelPath === '/' ? '' : levelPath}/${path === '/' ? '' : path}`;
|
|
53
54
|
|
|
54
55
|
return (
|
|
@@ -60,6 +61,7 @@ const Item = (props) => {
|
|
|
60
61
|
path={isExternal ? externalPath : absolutePath}
|
|
61
62
|
icon={isExternal ? <EarthIcon /> : <LinkIcon />}
|
|
62
63
|
isPublished={isPublished}
|
|
64
|
+
isExternal={isExternal}
|
|
63
65
|
onItemRemove={() => onItemRemove(item)}
|
|
64
66
|
onItemEdit={() => onItemEdit({
|
|
65
67
|
...item,
|
|
@@ -15,6 +15,7 @@ const List = ({
|
|
|
15
15
|
onItemLevelAdd,
|
|
16
16
|
onItemRemove,
|
|
17
17
|
onItemRestore,
|
|
18
|
+
displayFlat,
|
|
18
19
|
}) => (
|
|
19
20
|
<Wrapper level={level}>
|
|
20
21
|
{items.map((item, n) => {
|
|
@@ -34,6 +35,7 @@ const List = ({
|
|
|
34
35
|
onItemRemove={onItemRemove}
|
|
35
36
|
onItemEdit={onItemEdit}
|
|
36
37
|
error={error}
|
|
38
|
+
displayChildren={displayFlat}
|
|
37
39
|
/>
|
|
38
40
|
);
|
|
39
41
|
})}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import React, { useRef, useState, useEffect } from 'react';
|
|
2
|
+
import { useIntl } from 'react-intl';
|
|
3
|
+
import { IconButton } from '@strapi/design-system/IconButton';
|
|
4
|
+
import { Searchbar, SearchForm } from '@strapi/design-system/Searchbar';
|
|
5
|
+
import SearchIcon from "@strapi/icons/Search";
|
|
6
|
+
import { getTradId } from '../../translations';
|
|
7
|
+
|
|
8
|
+
const Search = ({ value, setValue }) => {
|
|
9
|
+
const [isOpen, setIsOpen] = useState(!!value);
|
|
10
|
+
const wrapperRef = useRef(null);
|
|
11
|
+
const { formatMessage } = useIntl();
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (isOpen) {
|
|
15
|
+
setTimeout(() => {
|
|
16
|
+
wrapperRef.current.querySelector('input').focus();
|
|
17
|
+
}, 0);
|
|
18
|
+
}
|
|
19
|
+
}, [isOpen]);
|
|
20
|
+
|
|
21
|
+
if (isOpen) {
|
|
22
|
+
return (
|
|
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>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
} else {
|
|
43
|
+
return (
|
|
44
|
+
<IconButton icon={<SearchIcon />} onClick={() => setIsOpen(!isOpen)} />
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default Search;
|
|
@@ -14,10 +14,8 @@ import { ContentLayout } from '@strapi/design-system/Layout';
|
|
|
14
14
|
import { Button } from '@strapi/design-system/Button';
|
|
15
15
|
import { LoadingIndicatorPage } from "@strapi/helper-plugin";
|
|
16
16
|
import { EmptyStateLayout } from '@strapi/design-system/EmptyStateLayout';
|
|
17
|
-
import { IconButton } from '@strapi/design-system/IconButton';
|
|
18
17
|
import EmptyDocumentsIcon from '@strapi/icons/EmptyDocuments';
|
|
19
18
|
import PlusIcon from "@strapi/icons/Plus";
|
|
20
|
-
import SearchIcon from "@strapi/icons/Search";
|
|
21
19
|
|
|
22
20
|
// Components
|
|
23
21
|
import List from '../../components/NavigationItemList';
|
|
@@ -32,6 +30,7 @@ import {
|
|
|
32
30
|
usedContentTypes,
|
|
33
31
|
validateNavigationStructure,
|
|
34
32
|
} from './utils/parsers';
|
|
33
|
+
import Search from '../../components/Search';
|
|
35
34
|
|
|
36
35
|
const View = () => {
|
|
37
36
|
const {
|
|
@@ -55,6 +54,9 @@ const View = () => {
|
|
|
55
54
|
const [activeNavigationItem, setActiveNavigationItemState] = useState({});
|
|
56
55
|
const { formatMessage } = useIntl();
|
|
57
56
|
|
|
57
|
+
const [searchValue, setSearchValue] = useState('');
|
|
58
|
+
const isSearchEmpty = isEmpty(searchValue);
|
|
59
|
+
|
|
58
60
|
const structureHasErrors = !validateNavigationStructure((changedActiveNavigation || {}).items);
|
|
59
61
|
const navigationSelectValue = get(activeNavigation, "id", null);
|
|
60
62
|
const handleSave = () => isLoadingForSubmit || structureHasErrors
|
|
@@ -104,6 +106,15 @@ const View = () => {
|
|
|
104
106
|
handleChangeNavigationData(changedStructure, true);
|
|
105
107
|
};
|
|
106
108
|
|
|
109
|
+
const filteredListFactory = (items, filterFunction) => items.reduce((acc, item) => {
|
|
110
|
+
const subItems = !isEmpty(item.items) ? filteredListFactory(item.items, filterFunction) : [];
|
|
111
|
+
if (filterFunction(item))
|
|
112
|
+
return [item, ...subItems, ...acc];
|
|
113
|
+
else
|
|
114
|
+
return [...subItems, ...acc];
|
|
115
|
+
}, []);
|
|
116
|
+
const filteredList = !isSearchEmpty ? filteredListFactory(changedActiveNavigation.items, (item) => item?.title.includes(searchValue)) : [];
|
|
117
|
+
|
|
107
118
|
const handleItemRemove = (item) => {
|
|
108
119
|
handleSubmitNavigationItem({
|
|
109
120
|
...item,
|
|
@@ -147,7 +158,7 @@ const View = () => {
|
|
|
147
158
|
{changedActiveNavigation && (
|
|
148
159
|
<>
|
|
149
160
|
<NavigationContentHeader
|
|
150
|
-
startActions={<
|
|
161
|
+
startActions={<Search value={searchValue} setValue={setSearchValue}/>}
|
|
151
162
|
endActions={<Button
|
|
152
163
|
onClick={addNewNavigationItem}
|
|
153
164
|
startIcon={<PlusIcon />}
|
|
@@ -176,11 +187,12 @@ const View = () => {
|
|
|
176
187
|
{
|
|
177
188
|
!isEmpty(changedActiveNavigation.items || [])
|
|
178
189
|
&& <List
|
|
179
|
-
items={changedActiveNavigation.items || []}
|
|
190
|
+
items={isSearchEmpty ? changedActiveNavigation.items || [] : filteredList}
|
|
180
191
|
onItemLevelAdd={addNewNavigationItem}
|
|
181
192
|
onItemRemove={handleItemRemove}
|
|
182
193
|
onItemEdit={handleItemEdit}
|
|
183
194
|
onItemRestore={handleItemRestore}
|
|
195
|
+
displayFlat={!isSearchEmpty}
|
|
184
196
|
root
|
|
185
197
|
error={error}
|
|
186
198
|
allowedLevels={config.allowedLevels}
|
|
@@ -28,7 +28,7 @@ export const transformItemToRESTPayload = (
|
|
|
28
28
|
const { contentTypes = [] } = config;
|
|
29
29
|
|
|
30
30
|
const parsedRelated = Number(related);
|
|
31
|
-
const relatedId = isExternal || isNaN(parsedRelated) ? related
|
|
31
|
+
const relatedId = isExternal || isNaN(parsedRelated) ? related?.value || related : parsedRelated;
|
|
32
32
|
|
|
33
33
|
const relatedContentType = relatedType ?
|
|
34
34
|
find(contentTypes,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "strapi-plugin-navigation",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.4",
|
|
4
4
|
"description": "Strapi - Navigation plugin",
|
|
5
5
|
"strapi": {
|
|
6
6
|
"name": "navigation",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"test:unit": "jest --verbose --coverage"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"
|
|
19
|
+
"uuid": "^8.3.0",
|
|
20
20
|
"bad-words": "^3.0.3",
|
|
21
21
|
"lodash": "^4.17.11",
|
|
22
22
|
"react": "^16.9.0",
|
|
@@ -38,6 +38,9 @@
|
|
|
38
38
|
"jest-styled-components": "^7.0.2",
|
|
39
39
|
"codecov": "^3.7.2"
|
|
40
40
|
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"@strapi/strapi": "4.x"
|
|
43
|
+
},
|
|
41
44
|
"author": {
|
|
42
45
|
"name": "VirtusLab // Mateusz Ziarko",
|
|
43
46
|
"email": "mziarko@virtuslab.com",
|
|
@@ -48,4 +48,25 @@ module.exports = {
|
|
|
48
48
|
return getService().put(id, body, auditLog)
|
|
49
49
|
.catch(errorHandler(ctx));
|
|
50
50
|
},
|
|
51
|
+
async render(ctx) {
|
|
52
|
+
const { params, query = {} } = ctx;
|
|
53
|
+
const { type, menu: menuOnly } = query;
|
|
54
|
+
const { idOrSlug } = parseParams(params);
|
|
55
|
+
return getService().render(
|
|
56
|
+
idOrSlug,
|
|
57
|
+
type,
|
|
58
|
+
menuOnly,
|
|
59
|
+
);
|
|
60
|
+
},
|
|
61
|
+
async renderChild(ctx) {
|
|
62
|
+
const { params, query = {} } = ctx;
|
|
63
|
+
const { type, menu: menuOnly } = query;
|
|
64
|
+
const { idOrSlug, childUIKey } = parseParams(params);
|
|
65
|
+
return getService().renderChildren(
|
|
66
|
+
idOrSlug,
|
|
67
|
+
childUIKey,
|
|
68
|
+
type,
|
|
69
|
+
menuOnly
|
|
70
|
+
);
|
|
71
|
+
},
|
|
51
72
|
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const getTypes = require('./types');
|
|
2
|
+
const getQueries = require('./queries');
|
|
3
|
+
const getResolversConfig = require('./resolvers-config');
|
|
4
|
+
|
|
5
|
+
module.exports = () => {
|
|
6
|
+
const extensionService = strapi.plugin('graphql').service('extension');
|
|
7
|
+
|
|
8
|
+
extensionService.shadowCRUD('plugin::navigation.audience').disable();
|
|
9
|
+
extensionService.shadowCRUD('plugin::navigation.navigation').disable();
|
|
10
|
+
extensionService.shadowCRUD('plugin::navigation.navigation-item').disable();
|
|
11
|
+
extensionService.shadowCRUD('plugin::navigation.navigations-items-related').disable();
|
|
12
|
+
|
|
13
|
+
extensionService.use(({ nexus }) => {
|
|
14
|
+
const types = getTypes({ strapi, nexus });
|
|
15
|
+
const queries = getQueries({ strapi, nexus });
|
|
16
|
+
const resolversConfig = getResolversConfig({ strapi });
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
types: [types, queries],
|
|
20
|
+
resolversConfig,
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module.exports = (context) => {
|
|
2
|
+
const queries = {
|
|
3
|
+
renderNavigationChild: require('./render-navigation-child'),
|
|
4
|
+
renderNavigation: require('./render-navigation'),
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
return context.nexus.extendType({
|
|
8
|
+
type: 'Query',
|
|
9
|
+
definition(t) {
|
|
10
|
+
for (const [name, configFactory] of Object.entries(queries)) {
|
|
11
|
+
const config = configFactory(context);
|
|
12
|
+
|
|
13
|
+
t.field(name, config);
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module.exports = ({ strapi, nexus }) => {
|
|
2
|
+
const { nonNull, list, stringArg, booleanArg } = nexus;
|
|
3
|
+
return {
|
|
4
|
+
type: nonNull(list('NavigationItem')),
|
|
5
|
+
args: {
|
|
6
|
+
id: nonNull(stringArg()),
|
|
7
|
+
childUiKey: nonNull(stringArg()),
|
|
8
|
+
type: 'NavigationRenderType',
|
|
9
|
+
menuOnly: booleanArg()
|
|
10
|
+
},
|
|
11
|
+
resolve(obj, args) {
|
|
12
|
+
const { id, childUIKey, type, menuOnly } = args;
|
|
13
|
+
return strapi.plugin('navigation').service('navigation').renderChildren(id, childUIKey, type, menuOnly);
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module.exports = ({ strapi, nexus }) => {
|
|
2
|
+
const { nonNull, list, stringArg, booleanArg } = nexus;
|
|
3
|
+
return {
|
|
4
|
+
type: nonNull(list('NavigationItem')),
|
|
5
|
+
args: {
|
|
6
|
+
navigationIdOrSlug: nonNull(stringArg()),
|
|
7
|
+
type: 'NavigationRenderType',
|
|
8
|
+
menuOnly: booleanArg()
|
|
9
|
+
},
|
|
10
|
+
resolve(obj, args) {
|
|
11
|
+
const { navigationIdOrSlug, type, menuOnly } = args;
|
|
12
|
+
return strapi.plugin('navigation').service('navigation').render(navigationIdOrSlug, type, menuOnly);
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
module.exports = ({ nexus }) => nexus.objectType({
|
|
2
|
+
name: "ContentTypesNameFields",
|
|
3
|
+
definition(t) {
|
|
4
|
+
t.nonNull.list.nonNull.string("default")
|
|
5
|
+
const contentTypesNameFields = strapi.plugin('navigation').config('contentTypesNameFields')
|
|
6
|
+
Object.keys(contentTypesNameFields || {}).forEach(key => t.nonNull.list.string(key))
|
|
7
|
+
}
|
|
8
|
+
})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module.exports = ({ nexus }) => nexus.objectType({
|
|
2
|
+
name: "ContentTypes",
|
|
3
|
+
definition(t) {
|
|
4
|
+
t.nonNull.string("uid")
|
|
5
|
+
t.nonNull.string("name")
|
|
6
|
+
t.nonNull.boolean("isSingle")
|
|
7
|
+
t.nonNull.string("collectionName")
|
|
8
|
+
t.nonNull.string("contentTypeName")
|
|
9
|
+
t.nonNull.string("label")
|
|
10
|
+
t.nonNull.string("relatedField")
|
|
11
|
+
t.nonNull.string("labelSingular")
|
|
12
|
+
t.nonNull.string("endpoint")
|
|
13
|
+
t.nonNull.boolean("available")
|
|
14
|
+
t.nonNull.boolean("visible")
|
|
15
|
+
}
|
|
16
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module.exports = ({ nexus }) => nexus.inputObjectType({
|
|
2
|
+
name: "CreateNavigationItem",
|
|
3
|
+
definition(t) {
|
|
4
|
+
t.nonNull.string("title")
|
|
5
|
+
t.nonNull.string("type")
|
|
6
|
+
t.string("path")
|
|
7
|
+
t.string("externalPath")
|
|
8
|
+
t.nonNull.string("uiRouterKey")
|
|
9
|
+
t.nonNull.boolean("menuAttached")
|
|
10
|
+
t.nonNull.int("order")
|
|
11
|
+
t.int("parent")
|
|
12
|
+
t.int("master")
|
|
13
|
+
t.list.field("items", { type: 'CreateNavigationItem' })
|
|
14
|
+
t.list.string("audience")
|
|
15
|
+
t.field("related", { type: 'CreateNavigationRelated' })
|
|
16
|
+
}
|
|
17
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const typesFactories = [
|
|
2
|
+
require('./navigation-item'),
|
|
3
|
+
require('./navigation-related'),
|
|
4
|
+
require('./navigation-render-type'),
|
|
5
|
+
require('./navigation'),
|
|
6
|
+
require('./navigation-details'),
|
|
7
|
+
require('./content-types-name-fields'),
|
|
8
|
+
require('./content-types'),
|
|
9
|
+
require('./navigation-config'),
|
|
10
|
+
require('./create-navigation-related'),
|
|
11
|
+
require('./create-navigation-item'),
|
|
12
|
+
require('./create-navigation'),
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
module.exports = context => typesFactories.map(factory => factory(context));
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
module.exports = ({ nexus }) => nexus.objectType({
|
|
2
|
+
name: "NavigationConfig",
|
|
3
|
+
definition(t) {
|
|
4
|
+
t.int("allowedLevels");
|
|
5
|
+
t.nonNull.list.string("additionalFields");
|
|
6
|
+
t.field("contentTypesNameFields", { type: 'ContentTypesNameFields' });
|
|
7
|
+
t.list.field("contentTypes", { type: 'ContentTypes' });
|
|
8
|
+
}
|
|
9
|
+
})
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module.exports = ({ nexus }) => nexus.objectType({
|
|
2
|
+
name: "NavigationDetails",
|
|
3
|
+
definition(t) {
|
|
4
|
+
t.nonNull.string("id")
|
|
5
|
+
t.nonNull.string("name")
|
|
6
|
+
t.nonNull.string("slug")
|
|
7
|
+
t.nonNull.boolean("visible")
|
|
8
|
+
t.nonNull.list.field("items", { type: 'NavigationItem' })
|
|
9
|
+
}
|
|
10
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module.exports = ({ nexus }) =>
|
|
2
|
+
nexus.objectType({
|
|
3
|
+
name: "NavigationItem",
|
|
4
|
+
definition(t) {
|
|
5
|
+
t.nonNull.int("id")
|
|
6
|
+
t.nonNull.string("title")
|
|
7
|
+
t.nonNull.string("type")
|
|
8
|
+
t.string("path")
|
|
9
|
+
t.string("externalPath")
|
|
10
|
+
t.nonNull.string("uiRouterKey")
|
|
11
|
+
t.nonNull.boolean("menuAttached")
|
|
12
|
+
t.nonNull.int("order")
|
|
13
|
+
t.int("parent")
|
|
14
|
+
t.int("master")
|
|
15
|
+
t.list.field("items", { type: 'NavigationItem' })
|
|
16
|
+
t.list.field("related", { type: 'NavigationRelated' })
|
|
17
|
+
t.list.string("audience")
|
|
18
|
+
// SQL
|
|
19
|
+
t.string("created_at")
|
|
20
|
+
t.string("updated_at")
|
|
21
|
+
t.string("created_by")
|
|
22
|
+
t.string("updated_by")
|
|
23
|
+
// MONGO
|
|
24
|
+
t.string("createdAt")
|
|
25
|
+
t.string("updatedAt")
|
|
26
|
+
t.string("createdBy")
|
|
27
|
+
t.string("updatedBy")
|
|
28
|
+
}
|
|
29
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module.exports = ({ strapi, nexus }) => {
|
|
2
|
+
const related = strapi.plugin('navigation').config('gql').navigationItemRelated;
|
|
3
|
+
const name = "NavigationRelated";
|
|
4
|
+
|
|
5
|
+
if (related?.length) {
|
|
6
|
+
return nexus.unionType({
|
|
7
|
+
name,
|
|
8
|
+
definition(t) {
|
|
9
|
+
t.members(...related)
|
|
10
|
+
},
|
|
11
|
+
resolveType: (item) => strapi.contentTypes[item.__contentType]?.globalId
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return nexus.objectType({
|
|
16
|
+
name,
|
|
17
|
+
definition(t) {
|
|
18
|
+
t.int("id")
|
|
19
|
+
t.string("title")
|
|
20
|
+
t.string("name")
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
type: 'content-api',
|
|
3
|
+
routes: [
|
|
4
|
+
{
|
|
5
|
+
method: "GET",
|
|
6
|
+
path: "/render/:idOrSlug",
|
|
7
|
+
handler: "navigation.render",
|
|
8
|
+
config: {
|
|
9
|
+
policies: []
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
method: "GET",
|
|
14
|
+
path: "/render/:idOrSlug/:childUIKey",
|
|
15
|
+
handler: "navigation.renderChild",
|
|
16
|
+
config: {
|
|
17
|
+
policies: []
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
package/server/routes/index.js
CHANGED
|
@@ -8,11 +8,18 @@ const {
|
|
|
8
8
|
last,
|
|
9
9
|
upperFirst,
|
|
10
10
|
map,
|
|
11
|
+
toNumber,
|
|
12
|
+
isString,
|
|
13
|
+
first,
|
|
14
|
+
|
|
11
15
|
} = require('lodash');
|
|
16
|
+
const { validate: isUuid } = require('uuid');
|
|
17
|
+
const slugify = require('slugify');
|
|
12
18
|
const { KIND_TYPES } = require('./utils/constant');
|
|
13
19
|
const utilsFunctionsFactory = require('./utils/functions');
|
|
14
|
-
const {
|
|
15
|
-
|
|
20
|
+
const { renderType } = require('../content-types/navigation/lifecycle');
|
|
21
|
+
const { type: itemType, additionalFields: configAdditionalFields } = require('../content-types/navigation-item').lifecycle;
|
|
22
|
+
const { NotFoundError } = require('@strapi/utils').errors
|
|
16
23
|
const excludedContentTypes = ['strapi::'];
|
|
17
24
|
const contentTypesNameFieldsDefaults = ['title', 'subject', 'name'];
|
|
18
25
|
|
|
@@ -37,7 +44,7 @@ module.exports = ({ strapi }) => {
|
|
|
37
44
|
const { masterModel, itemModel } = utilsFunctions.extractMeta(strapi.plugins);
|
|
38
45
|
const entity = await strapi
|
|
39
46
|
.query(masterModel.uid)
|
|
40
|
-
.findOne({ where: { id }});
|
|
47
|
+
.findOne({ where: { id } });
|
|
41
48
|
|
|
42
49
|
const entityItems = await strapi
|
|
43
50
|
.query(itemModel.uid)
|
|
@@ -193,9 +200,11 @@ module.exports = ({ strapi }) => {
|
|
|
193
200
|
.map(async ([model, related]) => {
|
|
194
201
|
const relationData = await strapi
|
|
195
202
|
.query(model)
|
|
196
|
-
.findMany({
|
|
197
|
-
|
|
198
|
-
|
|
203
|
+
.findMany({
|
|
204
|
+
where: {
|
|
205
|
+
id: { $in: map(related, 'related_id') }
|
|
206
|
+
}
|
|
207
|
+
});
|
|
199
208
|
return relationData
|
|
200
209
|
.flatMap(_ =>
|
|
201
210
|
Object.assign(
|
|
@@ -286,6 +295,263 @@ module.exports = ({ strapi }) => {
|
|
|
286
295
|
});
|
|
287
296
|
},
|
|
288
297
|
|
|
298
|
+
async renderChildren(
|
|
299
|
+
idOrSlug,
|
|
300
|
+
childUIKey,
|
|
301
|
+
type = renderType.FLAT,
|
|
302
|
+
menuOnly = false,
|
|
303
|
+
) {
|
|
304
|
+
const { service } = utilsFunctions.extractMeta(strapi.plugins);
|
|
305
|
+
const findById = !isNaN(toNumber(idOrSlug)) || isUuid(idOrSlug);
|
|
306
|
+
const criteria = findById ? { id: idOrSlug } : { slug: idOrSlug };
|
|
307
|
+
const filter = type === renderType.FLAT ? null : childUIKey;
|
|
308
|
+
|
|
309
|
+
const itemCriteria = {
|
|
310
|
+
...(menuOnly && { menuAttached: true }),
|
|
311
|
+
...(type === renderType.FLAT ? { uiRouterKey: childUIKey } : {}),
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
return service.renderType(type, criteria, itemCriteria, filter);
|
|
315
|
+
},
|
|
316
|
+
|
|
317
|
+
async render(idOrSlug, type = renderType.FLAT, menuOnly = false) {
|
|
318
|
+
const { service } = utilsFunctions.extractMeta(strapi.plugins);
|
|
319
|
+
|
|
320
|
+
const findById = !isNaN(toNumber(idOrSlug)) || isUuid(idOrSlug);
|
|
321
|
+
const criteria = findById ? { id: idOrSlug } : { slug: idOrSlug };
|
|
322
|
+
const itemCriteria = menuOnly ? { menuAttached: true } : {};
|
|
323
|
+
|
|
324
|
+
return service.renderType(type, criteria, itemCriteria);
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
async renderType(type = renderType.FLAT, criteria = {}, itemCriteria = {}, filter = null) {
|
|
328
|
+
const { pluginName, service, masterModel, itemModel } = utilsFunctions.extractMeta(
|
|
329
|
+
strapi.plugins,
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
const entity = await strapi
|
|
333
|
+
.query(masterModel.uid)
|
|
334
|
+
.findOne({
|
|
335
|
+
where: {
|
|
336
|
+
...criteria,
|
|
337
|
+
visible: true,
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
if (entity && entity.id) {
|
|
341
|
+
const entities = await strapi.query(itemModel.uid).findMany({
|
|
342
|
+
where: {
|
|
343
|
+
master: entity.id,
|
|
344
|
+
...itemCriteria,
|
|
345
|
+
},
|
|
346
|
+
paggination: {
|
|
347
|
+
limit: -1,
|
|
348
|
+
},
|
|
349
|
+
sort: ['order:asc'],
|
|
350
|
+
populate: ['related', 'audience', 'parent'],
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
if (!entities) {
|
|
354
|
+
return [];
|
|
355
|
+
}
|
|
356
|
+
const items = await this.getRelatedItems(entities);
|
|
357
|
+
const { contentTypes, contentTypesNameFields } = await service.config();
|
|
358
|
+
|
|
359
|
+
switch (type?.toLowerCase()) {
|
|
360
|
+
case renderType.TREE:
|
|
361
|
+
case renderType.RFR:
|
|
362
|
+
const getTemplateName = await utilsFunctions.templateNameFactory(items, strapi, contentTypes);
|
|
363
|
+
const itemParser = (item, path = '', field) => {
|
|
364
|
+
const isExternal = item.type === itemType.EXTERNAL;
|
|
365
|
+
const parentPath = isExternal ? undefined : `${path === '/' ? '' : path}/${item.path === '/'
|
|
366
|
+
? ''
|
|
367
|
+
: item.path}`;
|
|
368
|
+
const slug = isString(parentPath) ? slugify(
|
|
369
|
+
(first(parentPath) === '/' ? parentPath.substring(1) : parentPath).replace(/\//g, '-')) : undefined;
|
|
370
|
+
const lastRelated = item.related ? last(item.related) : undefined;
|
|
371
|
+
return {
|
|
372
|
+
id: item.id,
|
|
373
|
+
title: utilsFunctions.composeItemTitle(item, contentTypesNameFields, contentTypes),
|
|
374
|
+
menuAttached: item.menuAttached,
|
|
375
|
+
path: isExternal ? item.externalPath : parentPath,
|
|
376
|
+
type: item.type,
|
|
377
|
+
uiRouterKey: item.uiRouterKey,
|
|
378
|
+
slug: !slug && item.uiRouterKey ? slugify(item.uiRouterKey) : slug,
|
|
379
|
+
external: isExternal,
|
|
380
|
+
related: isExternal || !lastRelated ? undefined : {
|
|
381
|
+
...lastRelated,
|
|
382
|
+
__templateName: getTemplateName(lastRelated.relatedType || lastRelated.__contentType, lastRelated.id),
|
|
383
|
+
},
|
|
384
|
+
audience: !isEmpty(item.audience) ? item.audience.map(aItem => aItem.key) : undefined,
|
|
385
|
+
items: isExternal ? undefined : service.renderTree({
|
|
386
|
+
items,
|
|
387
|
+
id: item.id,
|
|
388
|
+
field,
|
|
389
|
+
path: parentPath,
|
|
390
|
+
itemParser,
|
|
391
|
+
}),
|
|
392
|
+
};
|
|
393
|
+
};
|
|
394
|
+
const treeStructure = service.renderTree({
|
|
395
|
+
items,
|
|
396
|
+
field: 'parent',
|
|
397
|
+
itemParser,
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
const filteredStructure = filter
|
|
401
|
+
? treeStructure.filter((item) => item.uiRouterKey === filter)
|
|
402
|
+
: treeStructure;
|
|
403
|
+
|
|
404
|
+
if (type === renderType.RFR) {
|
|
405
|
+
return service.renderRFR({
|
|
406
|
+
items: filteredStructure,
|
|
407
|
+
contentTypes,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
return filteredStructure;
|
|
411
|
+
default:
|
|
412
|
+
return items
|
|
413
|
+
.filter(utilsFunctions.filterOutUnpublished)
|
|
414
|
+
.map((item) => ({
|
|
415
|
+
...item,
|
|
416
|
+
audience: item.audience?.map(_ => _.key),
|
|
417
|
+
title: utilsFunctions.composeItemTitle(item, contentTypesNameFields, contentTypes),
|
|
418
|
+
related: item.related?.map(({ localizations, ...item }) => item),
|
|
419
|
+
items: null,
|
|
420
|
+
}));
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
throw new NotFoundError();
|
|
424
|
+
},
|
|
425
|
+
|
|
426
|
+
renderTree({
|
|
427
|
+
items = [],
|
|
428
|
+
id = null,
|
|
429
|
+
field = 'parent',
|
|
430
|
+
path = '',
|
|
431
|
+
itemParser = (i) => i,
|
|
432
|
+
}) {
|
|
433
|
+
return items
|
|
434
|
+
.filter(
|
|
435
|
+
(item) => {
|
|
436
|
+
if (item[field] === null && id === null) {
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
439
|
+
let data = item[field];
|
|
440
|
+
if (data && typeof id === 'string') {
|
|
441
|
+
data = data.toString();
|
|
442
|
+
}
|
|
443
|
+
return (data && data === id) || (isObject(item[field]) && (item[field].id === id));
|
|
444
|
+
},
|
|
445
|
+
)
|
|
446
|
+
.filter(utilsFunctions.filterOutUnpublished)
|
|
447
|
+
.map(item => itemParser({
|
|
448
|
+
...item,
|
|
449
|
+
}, path, field));
|
|
450
|
+
},
|
|
451
|
+
|
|
452
|
+
renderRFR({ items, parent = null, parentNavItem = null, contentTypes = [] }) {
|
|
453
|
+
const { service } = utilsFunctions.extractMeta(strapi.plugins);
|
|
454
|
+
let pages = {};
|
|
455
|
+
let nav = {};
|
|
456
|
+
let navItems = [];
|
|
457
|
+
|
|
458
|
+
items.forEach(item => {
|
|
459
|
+
const { items: itemChilds, ...itemProps } = item;
|
|
460
|
+
const itemNav = service.renderRFRNav(itemProps);
|
|
461
|
+
const itemPage = service.renderRFRPage({
|
|
462
|
+
item: itemProps,
|
|
463
|
+
parent,
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
if (item.type === itemType.INTERNAL) {
|
|
467
|
+
pages = {
|
|
468
|
+
...pages,
|
|
469
|
+
[itemPage.id]: {
|
|
470
|
+
...itemPage,
|
|
471
|
+
},
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (item.menuAttached) {
|
|
476
|
+
navItems.push(itemNav);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (!parent) {
|
|
480
|
+
nav = {
|
|
481
|
+
...nav,
|
|
482
|
+
root: navItems,
|
|
483
|
+
};
|
|
484
|
+
} else {
|
|
485
|
+
const navLevel = navItems
|
|
486
|
+
.filter(navItem => navItem.type === itemType.INTERNAL.toLowerCase());
|
|
487
|
+
if (!isEmpty(navLevel))
|
|
488
|
+
nav = {
|
|
489
|
+
...nav,
|
|
490
|
+
[parent]: [].concat(parentNavItem ? parentNavItem : [], navLevel),
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (!isEmpty(itemChilds)) {
|
|
495
|
+
const { nav: nestedNavs } = service.renderRFR({
|
|
496
|
+
items: itemChilds,
|
|
497
|
+
parent: itemPage.id,
|
|
498
|
+
parentNavItem: itemNav,
|
|
499
|
+
contentTypes,
|
|
500
|
+
});
|
|
501
|
+
const { pages: nestedPages } = service.renderRFR({
|
|
502
|
+
items: itemChilds.filter(child => child.type === itemType.INTERNAL),
|
|
503
|
+
parent: itemPage.id,
|
|
504
|
+
parentNavItem: itemNav,
|
|
505
|
+
contentTypes,
|
|
506
|
+
});
|
|
507
|
+
pages = {
|
|
508
|
+
...pages,
|
|
509
|
+
...nestedPages,
|
|
510
|
+
};
|
|
511
|
+
nav = {
|
|
512
|
+
...nav,
|
|
513
|
+
...nestedNavs,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
return {
|
|
519
|
+
pages,
|
|
520
|
+
nav,
|
|
521
|
+
};
|
|
522
|
+
},
|
|
523
|
+
|
|
524
|
+
renderRFRNav(item) {
|
|
525
|
+
const { uiRouterKey, title, path, type, audience } = item;
|
|
526
|
+
return {
|
|
527
|
+
label: title,
|
|
528
|
+
type: type.toLowerCase(),
|
|
529
|
+
page: type === itemType.INTERNAL ? uiRouterKey : undefined,
|
|
530
|
+
url: type === itemType.EXTERNAL ? path : undefined,
|
|
531
|
+
audience,
|
|
532
|
+
};
|
|
533
|
+
},
|
|
534
|
+
|
|
535
|
+
renderRFRPage({ item, parent }) {
|
|
536
|
+
const { uiRouterKey, title, path, slug, related, type, audience, menuAttached } = item;
|
|
537
|
+
const { __contentType, id, __templateName } = related || {};
|
|
538
|
+
const contentType = __contentType || '';
|
|
539
|
+
return {
|
|
540
|
+
id: uiRouterKey,
|
|
541
|
+
title,
|
|
542
|
+
templateName: __templateName,
|
|
543
|
+
related: type === itemType.INTERNAL ? {
|
|
544
|
+
contentType,
|
|
545
|
+
id,
|
|
546
|
+
} : undefined,
|
|
547
|
+
path,
|
|
548
|
+
slug,
|
|
549
|
+
parent,
|
|
550
|
+
audience,
|
|
551
|
+
menuAttached,
|
|
552
|
+
};
|
|
553
|
+
},
|
|
554
|
+
|
|
289
555
|
createBranch(items = [], masterEntity = null, parentItem = null, operations = {}) {
|
|
290
556
|
const { itemModel, service } = utilsFunctions.extractMeta(strapi.plugins);
|
|
291
557
|
return Promise.all(
|
|
@@ -2,9 +2,13 @@ const {
|
|
|
2
2
|
last,
|
|
3
3
|
isObject,
|
|
4
4
|
isEmpty,
|
|
5
|
+
flatten,
|
|
6
|
+
find,
|
|
7
|
+
isString,
|
|
8
|
+
get,
|
|
5
9
|
} = require('lodash');
|
|
6
10
|
|
|
7
|
-
const { type: itemType } = require('../../content-types/navigation-item');
|
|
11
|
+
const { type: itemType } = require('../../content-types/navigation-item/lifecycle');
|
|
8
12
|
const { NavigationError } = require('../../../utils/NavigationError');
|
|
9
13
|
const { TEMPLATE_DEFAULT } = require('./constant');
|
|
10
14
|
|
|
@@ -13,7 +17,7 @@ module.exports = ({ strapi }) => {
|
|
|
13
17
|
singularize(value = '') {
|
|
14
18
|
return last(value) === 's' ? value.substr(0, value.length - 1) : value;
|
|
15
19
|
},
|
|
16
|
-
|
|
20
|
+
|
|
17
21
|
extractMeta(plugins) {
|
|
18
22
|
const { navigation: plugin } = plugins;
|
|
19
23
|
const { navigation: service } = plugin.services;
|
|
@@ -99,5 +103,83 @@ module.exports = ({ strapi }) => {
|
|
|
99
103
|
return resolve();
|
|
100
104
|
});
|
|
101
105
|
},
|
|
106
|
+
|
|
107
|
+
async templateNameFactory(items, strapi, contentTypes = []) {
|
|
108
|
+
const flatRelated = flatten(items.map(i => i.related)).filter(_ => !!_);
|
|
109
|
+
const relatedMap = flatRelated.reduce((acc, curr) => {
|
|
110
|
+
if (!acc[curr.__contentType]) {
|
|
111
|
+
acc[curr.__contentType] = [];
|
|
112
|
+
}
|
|
113
|
+
acc[curr.__contentType].push(curr.id);
|
|
114
|
+
return acc;
|
|
115
|
+
}, {});
|
|
116
|
+
const responses = await Promise.all(
|
|
117
|
+
Object.entries(relatedMap)
|
|
118
|
+
.map(
|
|
119
|
+
([contentType, ids]) => {
|
|
120
|
+
const contentTypeUid = get(find(contentTypes, cnt => cnt.uid === contentType), 'uid');
|
|
121
|
+
return strapi.query(contentTypeUid)
|
|
122
|
+
.findMany({ id_in: ids, _limit: -1 })
|
|
123
|
+
.then(res => ({ [contentType]: res }))
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
126
|
+
const relatedResponseMap = responses.reduce((acc, curr) => ({ ...acc, ...curr }), {});
|
|
127
|
+
const singleTypes = new Map(
|
|
128
|
+
contentTypes
|
|
129
|
+
.filter(x => x.isSingle)
|
|
130
|
+
.map(({ contentTypeName, templateName }) => [contentTypeName, templateName || contentTypeName])
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
return (contentType, id) => {
|
|
134
|
+
const template = get(relatedResponseMap[contentType].find(data => data.id === id), 'template');
|
|
135
|
+
|
|
136
|
+
if (template) {
|
|
137
|
+
const templateComponent = this.getTemplateComponentFromTemplate(template);
|
|
138
|
+
return get(templateComponent, 'options.templateName', TEMPLATE_DEFAULT);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (singleTypes.get(contentType)) {
|
|
142
|
+
return singleTypes.get(contentType);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return TEMPLATE_DEFAULT;
|
|
146
|
+
};
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
getTemplateComponentFromTemplate(template = []) {
|
|
150
|
+
const componentName = get(first(template), '__component');
|
|
151
|
+
return componentName ? strapi.components[componentName] : null;
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
composeItemTitle(item = {}, fields = {}, contentTypes = []) {
|
|
155
|
+
const { title, related } = item;
|
|
156
|
+
if (title) {
|
|
157
|
+
return isString(title) && !isEmpty(title) ? title : undefined;
|
|
158
|
+
} else if (related) {
|
|
159
|
+
const relationTitle = this.extractItemRelationTitle(isArray(related) ? last(related) : related, fields, { contentTypes });
|
|
160
|
+
return isString(relationTitle) && !isEmpty(relationTitle) ? relationTitle : undefined;
|
|
161
|
+
}
|
|
162
|
+
return undefined;
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
extractItemRelationTitle(relatedItem = {}, fields = {}, contentTypes = []) {
|
|
166
|
+
const { __contentType } = relatedItem;
|
|
167
|
+
const contentType = find(contentTypes, _ => _.contentTypeName === __contentType);
|
|
168
|
+
const { default: defaultFields = [] } = fields;
|
|
169
|
+
return get(fields, `${contentType ? contentType.collectionName : ''}`, defaultFields).map((_) => relatedItem[_]).filter((_) => _)[0] || '';
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
filterOutUnpublished(item) {
|
|
173
|
+
const relatedItem = item.related && last(item.related);
|
|
174
|
+
const isHandledByPublshFlow = relatedItem ? 'published_at' in relatedItem : false;
|
|
175
|
+
|
|
176
|
+
if (isHandledByPublshFlow) {
|
|
177
|
+
const isRelatedDefinedAndPublished = relatedItem ?
|
|
178
|
+
isHandledByPublshFlow && get(relatedItem, 'published_at') :
|
|
179
|
+
false;
|
|
180
|
+
return item.type === itemType.INTERNAL ? isRelatedDefinedAndPublished : true;
|
|
181
|
+
}
|
|
182
|
+
return (item.type === itemType.EXTERNAL) || relatedItem;
|
|
183
|
+
},
|
|
102
184
|
};
|
|
103
185
|
}
|
package/strapi-server.js
CHANGED
|
@@ -4,6 +4,7 @@ const routes = require('./server/routes');
|
|
|
4
4
|
const controllers = require('./server/controllers');
|
|
5
5
|
const contentTypes = require('./server/content-types');
|
|
6
6
|
const config = require('./server/config');
|
|
7
|
+
const register = require('./server/register');
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
module.exports = () => {
|
|
@@ -13,6 +14,7 @@ module.exports = () => {
|
|
|
13
14
|
routes,
|
|
14
15
|
controllers,
|
|
15
16
|
services,
|
|
16
|
-
contentTypes
|
|
17
|
+
contentTypes,
|
|
18
|
+
register,
|
|
17
19
|
};
|
|
18
20
|
};
|