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.
Files changed (33) hide show
  1. package/README.md +62 -104
  2. package/admin/src/components/Item/ItemCardHeader/index.js +2 -2
  3. package/admin/src/components/Item/index.js +3 -1
  4. package/admin/src/components/NavigationItemList/index.js +2 -0
  5. package/admin/src/components/Search/index.js +49 -0
  6. package/admin/src/pages/View/index.js +16 -4
  7. package/admin/src/pages/View/utils/parsers.js +1 -1
  8. package/package.json +5 -2
  9. package/server/config/index.js +8 -0
  10. package/server/controllers/navigation.js +21 -0
  11. package/server/graphql/index.js +23 -0
  12. package/server/graphql/queries/index.js +17 -0
  13. package/server/graphql/queries/render-navigation-child.js +16 -0
  14. package/server/graphql/queries/render-navigation.js +15 -0
  15. package/server/graphql/resolvers-config.js +4 -0
  16. package/server/graphql/types/content-types-name-fields.js +8 -0
  17. package/server/graphql/types/content-types.js +16 -0
  18. package/server/graphql/types/create-navigation-item.js +17 -0
  19. package/server/graphql/types/create-navigation-related.js +8 -0
  20. package/server/graphql/types/create-navigation.js +7 -0
  21. package/server/graphql/types/index.js +15 -0
  22. package/server/graphql/types/navigation-config.js +9 -0
  23. package/server/graphql/types/navigation-details.js +10 -0
  24. package/server/graphql/types/navigation-item.js +29 -0
  25. package/server/graphql/types/navigation-related.js +23 -0
  26. package/server/graphql/types/navigation-render-type.js +4 -0
  27. package/server/graphql/types/navigation.js +9 -0
  28. package/server/register.js +5 -0
  29. package/server/routes/client.js +21 -0
  30. package/server/routes/index.js +2 -1
  31. package/server/services/navigation.js +272 -6
  32. package/server/services/utils/functions.js +84 -2
  33. package/strapi-server.js +3 -1
package/README.md CHANGED
@@ -1,34 +1,45 @@
1
- # Strapi v4 - Navigation plugin - BETA
2
-
3
- <p align="center">
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/npm/v/strapi-plugin-navigation/latest.svg" alt="NPM Version" />
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=master" alt="codecov.io" />
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
- </p>
16
+ </div>
17
+
18
+ ---
17
19
 
18
- A plugin for [Strapi Headless CMS](https://github.com/strapi/strapi) that provides navigation / menu builder feature with their possibility to control the audience and different output structure renderers:
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
- ### Versions
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
- ### ⏳ Installation
40
+ ## ⏳ Installation
30
41
 
31
- (Use **yarn** to install this plugin within your Strapi project (recommended). [Install yarn with these docs](https://yarnpkg.com/lang/en/docs/install/).)
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
- ### 🖐 Requirements
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/documentation/v3.x/installation/cli.html#step-1-make-sure-requirements-are-met">Installation Requirements</a>.
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.2 (recently tested)
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
- inside the `attributes` section like in example below:
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
- To setup the plugin properly we recommend to put following snippet as part of `config/custom.js` or `config/<env>/custom.js` file. If you've got already configurations for other plugins stores by this way, use just the `navigation` part within exising `plugins` item.
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
- plugins: {
112
- navigation: {
113
- additionalFields: ['audience'],
114
- allowedLevels: 2,
115
- contentTypesNameFields: {
116
- 'blog_posts': ['altTitle'],
117
- 'pages': ['title'],
118
- },
119
- gql: { ... }
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
- - `contentTypesNameFields` - Definition of content type title fields like `'content_type_name': ['field_name_1', 'field_name_2']`, if not set titles are pulled from fields like `['title', 'subject', 'name']`
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 which should contain union types which will be used for define GQL response format for your data while fetching:
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: 'union NavigationRelated = <your GQL related entities>',
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
- navigationItemRelated: 'union NavigationRelated = Pages | UploadFile',
129
+ navigationItemRelated: ['Page', 'UploadFile'],
153
130
  },
154
131
  ```
155
- where `Pages` and `UploadFile` are your types to the **Content Types** you're referring by navigation items relations.
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
- ## Audit log
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/) &amp; [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={<IconButton icon={<SearchIcon />} />}
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.value || related : parsedRelated;
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",
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
- "@types/uuid": "^8.3.0",
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",
@@ -0,0 +1,8 @@
1
+ module.exports = {
2
+ default: {
3
+ additionalFields: [],
4
+ contentTypes: [],
5
+ contentTypesNameFields: {},
6
+ allowedLevels: 2
7
+ }
8
+ }
@@ -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,4 @@
1
+ module.exports = ({ }) => ({
2
+ 'Query.renderNavigationChild': { auth: false },
3
+ 'Query.renderNavigation': { auth: false },
4
+ });
@@ -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,8 @@
1
+ module.exports = ({ nexus }) => nexus.inputObjectType({
2
+ name: "CreateNavigationRelated",
3
+ definition(t) {
4
+ t.nonNull.string("ref")
5
+ t.nonNull.string("field")
6
+ t.nonNull.string("refId")
7
+ }
8
+ });
@@ -0,0 +1,7 @@
1
+ module.exports = ({ nexus }) => nexus.inputObjectType({
2
+ name: "CreateNavigation",
3
+ definition(t) {
4
+ t.nonNull.string("name")
5
+ t.nonNull.list.field("items", { type: 'CreateNavigationItem' })
6
+ }
7
+ });
@@ -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,4 @@
1
+ module.exports = ({nexus}) => nexus.enumType({
2
+ name: "NavigationRenderType",
3
+ members: ['FLAT','TREE','RFR'],
4
+ });
@@ -0,0 +1,9 @@
1
+ module.exports = ({ nexus }) => nexus.objectType({
2
+ name: "Navigation",
3
+ definition(t) {
4
+ t.nonNull.string("id")
5
+ t.nonNull.string("name")
6
+ t.nonNull.string("slug")
7
+ t.nonNull.boolean("visible")
8
+ }
9
+ })
@@ -0,0 +1,5 @@
1
+ module.exports = ({ strapi }) => {
2
+ if (strapi.plugin('graphql')) {
3
+ require('./graphql')({ strapi });
4
+ }
5
+ };
@@ -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
+ }
@@ -1,3 +1,4 @@
1
1
  module.exports = {
2
- 'admin': require('./admin')
2
+ 'admin': require('./admin'),
3
+ 'content-api': require('./client'),
3
4
  };
@@ -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 { additionalFields: configAdditionalFields } = require('../content-types/navigation-item').lifecycle;
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({where: {
197
- id: { $in: map(related, 'related_id') }
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
  };