strapi-plugin-navigation 2.0.0-rc.1 → 2.0.0
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 +6 -4
- package/__mocks__/pages.settings.json +25 -0
- package/__mocks__/strapi.js +196 -0
- package/admin/src/components/Item/ItemCardBadge/index.js +1 -0
- package/admin/src/components/Item/ItemCardHeader/index.js +2 -1
- package/admin/src/components/Item/index.js +2 -3
- package/admin/src/components/Search/index.js +21 -23
- package/admin/src/pages/View/components/NavigationHeader/index.js +39 -23
- package/admin/src/pages/View/components/NavigationItemForm/index.js +13 -0
- package/admin/src/pages/View/components/NavigationItemPopup/NavigationItemPopupHeader.js +2 -4
- package/admin/src/pages/View/components/NavigationItemPopup/index.js +1 -1
- package/admin/src/pages/View/index.js +29 -20
- package/admin/src/pages/View/utils/parsers.js +7 -3
- package/admin/src/translations/en.json +5 -3
- package/package.json +5 -4
- package/server/content-types/navigation/schema.json +45 -0
- package/server/content-types/navigation-item/schema.json +1 -1
- package/server/services/__tests__/navigation.test.js +63 -78
- package/server/services/navigation.js +3 -3
- package/server/services/utils/functions.js +5 -12
- package/server/content-types/navigation/schema.js +0 -45
package/README.md
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
<div align="center">
|
|
1
|
+
<div align="center" width="150px">
|
|
2
2
|
<img style="width: 150px; height: auto;" src="public/assets/logo.png" alt="Logo - Strapi Navigation plugin" />
|
|
3
|
+
</div>
|
|
4
|
+
<div align="center">
|
|
3
5
|
<h1>Strapi v4 - Navigation plugin</h1>
|
|
4
|
-
<p>Create consumable navigation with a simple and
|
|
6
|
+
<p>Create consumable navigation with a simple and straightforward visual builder</p>
|
|
5
7
|
<a href="https://www.npmjs.org/package/strapi-plugin-navigation">
|
|
6
8
|
<img alt="GitHub package.json version" src="https://img.shields.io/github/package-json/v/VirtusLab-Open-Source/strapi-plugin-navigation?label=npm&logo=npm">
|
|
7
9
|
</a>
|
|
@@ -73,7 +75,7 @@ Complete installation requirements are exact same as for Strapi itself and can b
|
|
|
73
75
|
|
|
74
76
|
**Supported Strapi versions**:
|
|
75
77
|
|
|
76
|
-
- Strapi v4.0.
|
|
78
|
+
- Strapi v4.0.7 (recently tested)
|
|
77
79
|
- Strapi v4.x
|
|
78
80
|
|
|
79
81
|
> This plugin is designed for **Strapi v4** and is not working with v3.x. To get version for **Strapi v3** install version [v1.x](https://github.com/VirtusLab-Open-Source/strapi-plugin-navigation/tree/strapi-v3).
|
|
@@ -393,7 +395,7 @@ Feel free to fork and make a Pull Request to this plugin project. All the input
|
|
|
393
395
|
|
|
394
396
|
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:
|
|
395
397
|
|
|
396
|
-
- [
|
|
398
|
+
- [Discord](https://discord.strapi.io/) We're present on official Strapi Discord workspace. Find us by `[VirtusLab]` prefix and DM.
|
|
397
399
|
- [Slack - VirtusLab Open Source](https://virtuslab-oss.slack.com) We're present on a public channel #strapi-molecules
|
|
398
400
|
- [GitHub](https://github.com/VirtusLab/strapi-plugin-navigation/issues) (Bug reports, Contributions, Questions and Discussions)
|
|
399
401
|
- [E-mail](mailto:strapi@virtuslab.com) - we will respond back as soon as possible
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"kind": "collectionType",
|
|
3
|
+
"collectionName": "pages",
|
|
4
|
+
"info": {
|
|
5
|
+
"singularName": "page",
|
|
6
|
+
"pluralName": "pages",
|
|
7
|
+
"displayName": "Page",
|
|
8
|
+
"name": "page"
|
|
9
|
+
},
|
|
10
|
+
"options": {
|
|
11
|
+
"increments": true,
|
|
12
|
+
"timestamps": true,
|
|
13
|
+
"searchable": true,
|
|
14
|
+
"previewable": true,
|
|
15
|
+
"draftAndPublish": false
|
|
16
|
+
},
|
|
17
|
+
"pluginOptions": {},
|
|
18
|
+
"attributes": {
|
|
19
|
+
"title": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"required": true
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
const { isMatch } = require('lodash');
|
|
2
|
+
|
|
3
|
+
const masterModelMock = {
|
|
4
|
+
findOne: () => ({
|
|
5
|
+
id: 1,
|
|
6
|
+
name: "Main navigation",
|
|
7
|
+
slug: "main-navigation",
|
|
8
|
+
visible: true,
|
|
9
|
+
createdAt: "2021-12-30T14:05:50.276Z",
|
|
10
|
+
updatedAt: "2021-12-30T14:05:50.276Z",
|
|
11
|
+
}),
|
|
12
|
+
findMany: () => [{
|
|
13
|
+
id: 1,
|
|
14
|
+
name: "Main navigation",
|
|
15
|
+
slug: "main-navigation",
|
|
16
|
+
visible: true,
|
|
17
|
+
createdAt: "2021-12-30T14:05:50.276Z",
|
|
18
|
+
updatedAt: "2021-12-30T14:05:50.276Z",
|
|
19
|
+
}],
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const itemModelMock = {
|
|
23
|
+
findOne: async () => ({
|
|
24
|
+
id: 1,
|
|
25
|
+
title: "home",
|
|
26
|
+
type: "INTERNAL",
|
|
27
|
+
path: "home1",
|
|
28
|
+
externalPath: null,
|
|
29
|
+
uiRouterKey: "home",
|
|
30
|
+
menuAttached: true,
|
|
31
|
+
order: 1,
|
|
32
|
+
createdAt: "2021-12-31T10:04:54.812Z",
|
|
33
|
+
updatedAt: "2022-01-14T13:36:29.430Z",
|
|
34
|
+
related: {
|
|
35
|
+
id: 1,
|
|
36
|
+
related_id: "1",
|
|
37
|
+
related_type: "api::pages.pages",
|
|
38
|
+
field: "navigation",
|
|
39
|
+
order: 1,
|
|
40
|
+
master: "3",
|
|
41
|
+
createdAt: "2021-12-31T10:04:54.800Z",
|
|
42
|
+
updatedAt: "2021-12-31T10:04:54.800Z",
|
|
43
|
+
navigationItemId: 56,
|
|
44
|
+
},
|
|
45
|
+
parent: null,
|
|
46
|
+
}),
|
|
47
|
+
findMany: async ({where}) => [{
|
|
48
|
+
id: 1,
|
|
49
|
+
title: "home",
|
|
50
|
+
type: "INTERNAL",
|
|
51
|
+
path: "home1",
|
|
52
|
+
externalPath: null,
|
|
53
|
+
uiRouterKey: "home",
|
|
54
|
+
menuAttached: true,
|
|
55
|
+
order: 1,
|
|
56
|
+
createdAt: "2021-12-31T10:04:54.812Z",
|
|
57
|
+
updatedAt: "2022-01-14T13:36:29.430Z",
|
|
58
|
+
master: 1,
|
|
59
|
+
related: {
|
|
60
|
+
id: 1,
|
|
61
|
+
related_id: "1",
|
|
62
|
+
related_type: "api::pages.pages",
|
|
63
|
+
field: "navigation",
|
|
64
|
+
order: 1,
|
|
65
|
+
master: "3",
|
|
66
|
+
createdAt: "2021-12-31T10:04:54.800Z",
|
|
67
|
+
updatedAt: "2021-12-31T10:04:54.800Z",
|
|
68
|
+
navigationItemId: 56,
|
|
69
|
+
},
|
|
70
|
+
parent: null,
|
|
71
|
+
}, {
|
|
72
|
+
id: 2,
|
|
73
|
+
title: "side",
|
|
74
|
+
type: "INTERNAL",
|
|
75
|
+
path: "side",
|
|
76
|
+
externalPath: null,
|
|
77
|
+
uiRouterKey: "side",
|
|
78
|
+
menuAttached: false,
|
|
79
|
+
order: 1,
|
|
80
|
+
createdAt: "2021-12-31T10:04:54.824Z",
|
|
81
|
+
updatedAt: "2021-12-31T12:47:20.508Z",
|
|
82
|
+
master: 1,
|
|
83
|
+
related: {
|
|
84
|
+
id: 2,
|
|
85
|
+
related_id: "2",
|
|
86
|
+
related_type: "api::pages.pages",
|
|
87
|
+
field: "navigation",
|
|
88
|
+
order: 1,
|
|
89
|
+
master: "3",
|
|
90
|
+
createdAt: "2021-12-31T10:04:54.823Z",
|
|
91
|
+
updatedAt: "2021-12-31T10:04:54.823Z",
|
|
92
|
+
navigationItemId: 57,
|
|
93
|
+
},
|
|
94
|
+
parent: {
|
|
95
|
+
id: 1,
|
|
96
|
+
title: "home",
|
|
97
|
+
type: "INTERNAL",
|
|
98
|
+
path: "home1",
|
|
99
|
+
externalPath: null,
|
|
100
|
+
uiRouterKey: "home",
|
|
101
|
+
menuAttached: true,
|
|
102
|
+
order: 1,
|
|
103
|
+
createdAt: "2021-12-31T10:04:54.812Z",
|
|
104
|
+
updatedAt: "2022-01-14T13:36:29.430Z",
|
|
105
|
+
},
|
|
106
|
+
}].filter(item => isMatch(item, where)),
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const pageModelMock = {
|
|
110
|
+
findOne: async ({ where }) => ({
|
|
111
|
+
"id": 1,
|
|
112
|
+
"attributes": {
|
|
113
|
+
"title": "Page nr 1",
|
|
114
|
+
"createdAt": "2022-01-19T08:22:31.244Z",
|
|
115
|
+
"updatedAt": "2022-01-19T08:22:31.244Z",
|
|
116
|
+
"publishedAt": null
|
|
117
|
+
}
|
|
118
|
+
}),
|
|
119
|
+
findMany: async ({ where }) => [{
|
|
120
|
+
"id": 1,
|
|
121
|
+
"attributes": {
|
|
122
|
+
"title": "Page nr 1",
|
|
123
|
+
"createdAt": "2022-01-19T08:22:31.244Z",
|
|
124
|
+
"updatedAt": "2022-01-19T08:22:31.244Z",
|
|
125
|
+
"publishedAt": null
|
|
126
|
+
}
|
|
127
|
+
}, {
|
|
128
|
+
"id": 2,
|
|
129
|
+
"attributes": {
|
|
130
|
+
"title": "Page nr 2",
|
|
131
|
+
"createdAt": "2022-01-19T08:22:50.821Z",
|
|
132
|
+
"updatedAt": "2022-01-19T08:22:50.821Z",
|
|
133
|
+
"publishedAt": null
|
|
134
|
+
}
|
|
135
|
+
}]
|
|
136
|
+
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const plugins = (strapi) => ({
|
|
140
|
+
navigation: {
|
|
141
|
+
get services() { return require('../server/services') },
|
|
142
|
+
service: (key) => (require('../server/services'))[key]({ strapi }),
|
|
143
|
+
get contentTypes() { return require('../server/content-types') },
|
|
144
|
+
contentType: (key) => preparePluginContentType(require('../server/content-types')[key].schema, 'navigation'),
|
|
145
|
+
config: (key) => ({
|
|
146
|
+
...require('../server/config').default,
|
|
147
|
+
contentTypes: ['api::pages.pages'],
|
|
148
|
+
})[key],
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const contentTypes = {
|
|
153
|
+
'api::pages.pages': {
|
|
154
|
+
...require('./pages.settings.json'),
|
|
155
|
+
uid: 'api::pages.pages',
|
|
156
|
+
modelName: 'page',
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const preparePluginContentType = (schema, plugin) => {
|
|
161
|
+
const { name } = schema.info;
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
...schema,
|
|
165
|
+
uid: `plugin::${plugin}.${name}`,
|
|
166
|
+
modelName: name,
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const strapiFactory = (plugins, contentTypes) => ({
|
|
171
|
+
get plugins() { return plugins(strapi) },
|
|
172
|
+
plugin: (name) => plugins(strapi)[name],
|
|
173
|
+
get contentTypes() { return contentTypes },
|
|
174
|
+
contentType: (key) => contentTypes[key],
|
|
175
|
+
query: (model) => {
|
|
176
|
+
switch (model) {
|
|
177
|
+
case 'plugin::navigation.navigation':
|
|
178
|
+
return masterModelMock;
|
|
179
|
+
case 'plugin::navigation.navigation-item':
|
|
180
|
+
return itemModelMock;
|
|
181
|
+
case 'api::pages.pages':
|
|
182
|
+
return pageModelMock;
|
|
183
|
+
default:
|
|
184
|
+
return {
|
|
185
|
+
findOne: () => ({}),
|
|
186
|
+
findMany: () => [],
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const setupStrapi = () => {
|
|
193
|
+
Object.defineProperty(global, 'strapi', { value: strapiFactory(plugins, contentTypes) });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
module.exports = { setupStrapi };
|
|
@@ -4,6 +4,7 @@ import { useIntl } from 'react-intl';
|
|
|
4
4
|
import { Flex } from '@strapi/design-system/Flex';
|
|
5
5
|
import { IconButton } from '@strapi/design-system/IconButton';
|
|
6
6
|
import { Typography } from '@strapi/design-system/Typography';
|
|
7
|
+
import { Icon } from '@strapi/design-system/Icon';
|
|
7
8
|
import PencilIcon from '@strapi/icons/Pencil';
|
|
8
9
|
import TrashIcon from '@strapi/icons/Trash';
|
|
9
10
|
import RefreshIcon from '@strapi/icons/Refresh';
|
|
@@ -19,7 +20,7 @@ const ItemCardHeader = ({ title, path, icon, removed, onItemRemove, onItemEdit,
|
|
|
19
20
|
return (
|
|
20
21
|
<Wrapper>
|
|
21
22
|
<Flex alignItems="center">
|
|
22
|
-
{icon}
|
|
23
|
+
<Icon as={icon} />
|
|
23
24
|
<Typography variant="omega" fontWeight="bold">
|
|
24
25
|
{title}
|
|
25
26
|
</Typography>
|
|
@@ -71,7 +71,7 @@ const Item = (props) => {
|
|
|
71
71
|
<ItemCardHeader
|
|
72
72
|
title={title}
|
|
73
73
|
path={isExternal ? externalPath : absolutePath}
|
|
74
|
-
icon={isExternal ?
|
|
74
|
+
icon={isExternal ? Earth : LinkIcon }
|
|
75
75
|
onItemRemove={() => onItemRemove({
|
|
76
76
|
...item,
|
|
77
77
|
relatedRef,
|
|
@@ -102,7 +102,6 @@ const Item = (props) => {
|
|
|
102
102
|
</TextButton>
|
|
103
103
|
{ relatedItemLabel && (<Box>
|
|
104
104
|
<ItemCardBadge
|
|
105
|
-
style={{ marginRight: 4 }}
|
|
106
105
|
borderColor={`${relatedBadgeColor}200`}
|
|
107
106
|
backgroundColor={`${relatedBadgeColor}100`}
|
|
108
107
|
textColor={`${relatedBadgeColor}600`}
|
|
@@ -131,7 +130,7 @@ const Item = (props) => {
|
|
|
131
130
|
onItemRestore={onItemRestore}
|
|
132
131
|
error={error}
|
|
133
132
|
allowedLevels={allowedLevels}
|
|
134
|
-
isParentAttachedToMenu={
|
|
133
|
+
isParentAttachedToMenu={menuAttached}
|
|
135
134
|
items={item.items}
|
|
136
135
|
level={level + 1}
|
|
137
136
|
levelPath={absolutePath}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { useRef, useState, useEffect } from 'react';
|
|
2
2
|
import { useIntl } from 'react-intl';
|
|
3
3
|
import { IconButton } from '@strapi/design-system/IconButton';
|
|
4
|
-
import { Searchbar
|
|
4
|
+
import { Searchbar } from '@strapi/design-system/Searchbar';
|
|
5
5
|
import SearchIcon from "@strapi/icons/Search";
|
|
6
6
|
import { getTradId } from '../../translations';
|
|
7
7
|
|
|
@@ -11,32 +11,30 @@ const Search = ({ value, setValue }) => {
|
|
|
11
11
|
const { formatMessage } = useIntl();
|
|
12
12
|
|
|
13
13
|
useEffect(() => {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
if (isOpen) {
|
|
15
|
+
setTimeout(() => {
|
|
16
|
+
wrapperRef.current.querySelector('input').focus();
|
|
17
|
+
}, 0);
|
|
18
|
+
}
|
|
19
19
|
}, [isOpen]);
|
|
20
|
-
|
|
20
|
+
|
|
21
21
|
if (isOpen) {
|
|
22
22
|
return (
|
|
23
23
|
<div ref={wrapperRef}>
|
|
24
|
-
<
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
placeholder
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
</Searchbar>
|
|
39
|
-
</SearchForm>
|
|
24
|
+
<Searchbar
|
|
25
|
+
name="searchbar"
|
|
26
|
+
onClear={() => { setValue(''); setIsOpen(false); }}
|
|
27
|
+
value={value}
|
|
28
|
+
size="S"
|
|
29
|
+
onChange={(e) => setValue(e.target.value)}
|
|
30
|
+
clearLabel="Clearing the search"
|
|
31
|
+
placeholder={formatMessage({
|
|
32
|
+
id: getTradId('popup.item.form.audience.placeholder'),
|
|
33
|
+
defaultMessage: 'Type to start searching...',
|
|
34
|
+
})}
|
|
35
|
+
>
|
|
36
|
+
Search for navigation items
|
|
37
|
+
</Searchbar>
|
|
40
38
|
</div>
|
|
41
39
|
);
|
|
42
40
|
} else {
|
|
@@ -7,43 +7,59 @@ import Check from '@strapi/icons/Check';
|
|
|
7
7
|
import More from '@strapi/icons/More';
|
|
8
8
|
import { getTrad } from '../../../../translations';
|
|
9
9
|
import { MoreButton } from './styles';
|
|
10
|
-
|
|
10
|
+
import { Select, Option } from '@strapi/design-system/Select';
|
|
11
|
+
import { Box } from '@strapi/design-system/Box'
|
|
11
12
|
|
|
12
13
|
const NavigationHeader = ({
|
|
14
|
+
activeNavigation,
|
|
15
|
+
availableNavigations,
|
|
13
16
|
structureHasErrors,
|
|
14
|
-
|
|
17
|
+
structureHasChanged,
|
|
18
|
+
handleChangeSelection,
|
|
15
19
|
handleSave,
|
|
16
20
|
}) => {
|
|
17
21
|
const { formatMessage } = useIntl();
|
|
18
|
-
|
|
19
22
|
return (
|
|
20
23
|
<HeaderLayout
|
|
21
|
-
|
|
24
|
+
primaryAction={
|
|
22
25
|
<Stack horizontal size={2}>
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
<Box width="10vw">
|
|
27
|
+
<Select
|
|
28
|
+
type="select"
|
|
29
|
+
placeholder={'Change navigation'}
|
|
30
|
+
name={`navigationSelect`}
|
|
31
|
+
onChange={handleChangeSelection}
|
|
32
|
+
value={activeNavigation?.id}
|
|
33
|
+
size="S"
|
|
34
|
+
style={null}
|
|
28
35
|
>
|
|
29
|
-
{
|
|
30
|
-
</
|
|
31
|
-
|
|
36
|
+
{availableNavigations.map(({ id, name }) => <Option key={id} value={id}>{name}</Option>)}
|
|
37
|
+
</Select >
|
|
38
|
+
</Box>
|
|
39
|
+
<Button
|
|
40
|
+
onClick={handleSave}
|
|
41
|
+
startIcon={<Check />}
|
|
42
|
+
disabled={structureHasErrors || !structureHasChanged}
|
|
43
|
+
type="submit"
|
|
44
|
+
>
|
|
45
|
+
{formatMessage(getTrad('submit.cta.save'))}
|
|
46
|
+
</Button>
|
|
47
|
+
{/* <MoreButton
|
|
32
48
|
id="more"
|
|
33
49
|
label="More"
|
|
34
50
|
icon={<More />}
|
|
35
51
|
/> */}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
52
|
+
</Stack>
|
|
53
|
+
}
|
|
54
|
+
title={formatMessage({
|
|
55
|
+
id: getTrad('header.title'),
|
|
56
|
+
defaultMessage: 'UI Navigation',
|
|
57
|
+
})}
|
|
58
|
+
subtitle={formatMessage({
|
|
59
|
+
id: getTrad('header.description'),
|
|
60
|
+
defaultMessage: 'Define your portal navigation',
|
|
61
|
+
})}
|
|
62
|
+
/>
|
|
47
63
|
);
|
|
48
64
|
};
|
|
49
65
|
|
|
@@ -259,6 +259,10 @@ const NavigationItemForm = ({
|
|
|
259
259
|
}}
|
|
260
260
|
name={`${inputsPrefix}title`}
|
|
261
261
|
placeholder={{
|
|
262
|
+
id: "e.g. Blog",
|
|
263
|
+
defaultMessage: 'e.g. Blog',
|
|
264
|
+
}}
|
|
265
|
+
description={{
|
|
262
266
|
id: getTradId('popup.item.form.title.placeholder'),
|
|
263
267
|
defaultMessage: 'e.g. Blog',
|
|
264
268
|
}}
|
|
@@ -331,6 +335,15 @@ const NavigationItemForm = ({
|
|
|
331
335
|
onChange={onChangeRelatedType}
|
|
332
336
|
options={relatedTypeSelectOptions}
|
|
333
337
|
value={relatedTypeSelectValue}
|
|
338
|
+
disabled={isLoading || isEmpty(relatedTypeSelectOptions)}
|
|
339
|
+
description={
|
|
340
|
+
!isLoading && isEmpty(relatedTypeSelectOptions)
|
|
341
|
+
? {
|
|
342
|
+
id: getTradId('popup.item.form.relatedType.empty'),
|
|
343
|
+
defaultMessage: 'There are no more content types',
|
|
344
|
+
}
|
|
345
|
+
: undefined
|
|
346
|
+
}
|
|
334
347
|
/>
|
|
335
348
|
</GridItem>
|
|
336
349
|
{relatedTypeSelectValue && !isSingleSelected && (
|
|
@@ -6,14 +6,12 @@ import { ModalHeader } from '@strapi/design-system/ModalLayout';
|
|
|
6
6
|
import { useIntl } from 'react-intl';
|
|
7
7
|
import { getTrad } from '../../../../translations';
|
|
8
8
|
|
|
9
|
-
export const NavigationItemPopupHeader = () => {
|
|
9
|
+
export const NavigationItemPopupHeader = ({isNewItem}) => {
|
|
10
10
|
const { formatMessage } = useIntl();
|
|
11
11
|
return (
|
|
12
12
|
<ModalHeader>
|
|
13
13
|
<ButtonText textColor="neutral800" as="h2" id="asset-dialog-title">
|
|
14
|
-
{formatMessage(
|
|
15
|
-
getTrad('popup.item.header'),
|
|
16
|
-
)}
|
|
14
|
+
{formatMessage(getTrad(`popup.item.header.${isNewItem ? 'new' : 'edit'}`))}
|
|
17
15
|
</ButtonText>
|
|
18
16
|
</ModalHeader>
|
|
19
17
|
);
|
|
@@ -80,7 +80,7 @@ const NavigationItemPopUp = ({
|
|
|
80
80
|
|
|
81
81
|
return (
|
|
82
82
|
<ModalLayout labelledBy="condition-modal-breadcrumbs" onClose={onClose} isOpen={isOpen}>
|
|
83
|
-
<NavigationItemPopupHeader />
|
|
83
|
+
<NavigationItemPopupHeader isNewItem={!data.viewId}/>
|
|
84
84
|
<NavigationItemForm
|
|
85
85
|
data={prepareFormData(data)}
|
|
86
86
|
isLoading={isLoading}
|
|
@@ -10,11 +10,13 @@ import { isEmpty, get } from "lodash";
|
|
|
10
10
|
|
|
11
11
|
// Design System
|
|
12
12
|
import { Main } from '@strapi/design-system/Main';
|
|
13
|
+
import { Flex } from '@strapi/design-system/Flex';
|
|
13
14
|
import { ContentLayout } from '@strapi/design-system/Layout';
|
|
15
|
+
import { Typography } from '@strapi/design-system/Typography';
|
|
14
16
|
import { Box } from '@strapi/design-system/Box';
|
|
17
|
+
import { Icon } from '@strapi/design-system/Icon';
|
|
15
18
|
import { Button } from '@strapi/design-system/Button';
|
|
16
19
|
import { LoadingIndicatorPage } from "@strapi/helper-plugin";
|
|
17
|
-
import { EmptyStateLayout } from '@strapi/design-system/EmptyStateLayout';
|
|
18
20
|
import EmptyDocumentsIcon from '@strapi/icons/EmptyDocuments';
|
|
19
21
|
import PlusIcon from "@strapi/icons/Plus";
|
|
20
22
|
|
|
@@ -23,6 +25,7 @@ import List from '../../components/NavigationItemList';
|
|
|
23
25
|
import NavigationContentHeader from './components/NavigationContentHeader';
|
|
24
26
|
import NavigationHeader from './components/NavigationHeader';
|
|
25
27
|
import NavigationItemPopUp from "./components/NavigationItemPopup";
|
|
28
|
+
import Search from '../../components/Search';
|
|
26
29
|
import useDataManager from "../../hooks/useDataManager";
|
|
27
30
|
import { getTrad } from '../../translations';
|
|
28
31
|
import {
|
|
@@ -31,7 +34,6 @@ import {
|
|
|
31
34
|
usedContentTypes,
|
|
32
35
|
validateNavigationStructure,
|
|
33
36
|
} from './utils/parsers';
|
|
34
|
-
import Search from '../../components/Search';
|
|
35
37
|
|
|
36
38
|
const View = () => {
|
|
37
39
|
const {
|
|
@@ -150,11 +152,19 @@ const View = () => {
|
|
|
150
152
|
changeNavigationItemPopupState(false);
|
|
151
153
|
};
|
|
152
154
|
|
|
155
|
+
const handleChangeNavigationSelection = (...args) => {
|
|
156
|
+
handleChangeSelection(...args);
|
|
157
|
+
setSearchValue('');
|
|
158
|
+
}
|
|
159
|
+
|
|
153
160
|
return (
|
|
154
161
|
<Main labelledBy="title" aria-busy={isLoadingForSubmit}>
|
|
155
162
|
<NavigationHeader
|
|
156
163
|
structureHasErrors={structureHasErrors}
|
|
157
|
-
|
|
164
|
+
structureHasChanged={structureChanged}
|
|
165
|
+
availableNavigations={availableNavigations}
|
|
166
|
+
activeNavigation={activeNavigation}
|
|
167
|
+
handleChangeSelection={handleChangeNavigationSelection}
|
|
158
168
|
handleSave={handleSave}
|
|
159
169
|
/>
|
|
160
170
|
<ContentLayout>
|
|
@@ -162,7 +172,7 @@ const View = () => {
|
|
|
162
172
|
{changedActiveNavigation && (
|
|
163
173
|
<>
|
|
164
174
|
<NavigationContentHeader
|
|
165
|
-
startActions={<Search value={searchValue} setValue={setSearchValue}/>}
|
|
175
|
+
startActions={<Search value={searchValue} setValue={setSearchValue} />}
|
|
166
176
|
endActions={<Button
|
|
167
177
|
onClick={addNewNavigationItem}
|
|
168
178
|
startIcon={<PlusIcon />}
|
|
@@ -172,22 +182,21 @@ const View = () => {
|
|
|
172
182
|
{formatMessage(getTrad('header.action.newItem'))}
|
|
173
183
|
</Button>}
|
|
174
184
|
/>
|
|
175
|
-
{isEmpty(changedActiveNavigation.items || []) && (
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
</Box>
|
|
185
|
+
{isEmpty(changedActiveNavigation.items || []) && (
|
|
186
|
+
<Flex direction="column" minHeight="400px" justifyContent="center">
|
|
187
|
+
<Icon as={EmptyDocumentsIcon} width="160px" height="88px" color=""/>
|
|
188
|
+
<Box padding={4}>
|
|
189
|
+
<Typography variant="beta" textColor="neutral600">{formatMessage(getTrad('empty'))}</Typography>
|
|
190
|
+
</Box>
|
|
191
|
+
<Button
|
|
192
|
+
variant='secondary'
|
|
193
|
+
startIcon={<PlusIcon />}
|
|
194
|
+
label={formatMessage(getTrad('empty.cta'))}
|
|
195
|
+
onClick={addNewNavigationItem}
|
|
196
|
+
>
|
|
197
|
+
{formatMessage(getTrad('empty.cta'))}
|
|
198
|
+
</Button>
|
|
199
|
+
</Flex>
|
|
191
200
|
)}
|
|
192
201
|
{
|
|
193
202
|
!isEmpty(changedActiveNavigation.items || [])
|
|
@@ -7,6 +7,7 @@ export const transformItemToRESTPayload = (
|
|
|
7
7
|
parent = undefined,
|
|
8
8
|
master = undefined,
|
|
9
9
|
config = {},
|
|
10
|
+
parentAttachedToMenu = true,
|
|
10
11
|
) => {
|
|
11
12
|
const {
|
|
12
13
|
id,
|
|
@@ -34,7 +35,7 @@ export const transformItemToRESTPayload = (
|
|
|
34
35
|
find(contentTypes,
|
|
35
36
|
ct => ct.uid === relatedType) :
|
|
36
37
|
undefined;
|
|
37
|
-
|
|
38
|
+
const itemAttachedToMenu = menuAttached && parentAttachedToMenu
|
|
38
39
|
return {
|
|
39
40
|
id,
|
|
40
41
|
parent,
|
|
@@ -45,7 +46,7 @@ export const transformItemToRESTPayload = (
|
|
|
45
46
|
removed,
|
|
46
47
|
order,
|
|
47
48
|
uiRouterKey,
|
|
48
|
-
menuAttached,
|
|
49
|
+
menuAttached: itemAttachedToMenu,
|
|
49
50
|
audience: audience.map((audienceItem) =>
|
|
50
51
|
isObject(audienceItem) ? audienceItem.value : audienceItem,
|
|
51
52
|
),
|
|
@@ -60,7 +61,7 @@ export const transformItemToRESTPayload = (
|
|
|
60
61
|
field: relatedContentType && relatedContentType.relatedField ? relatedContentType.relatedField : 'navigation',
|
|
61
62
|
},
|
|
62
63
|
],
|
|
63
|
-
items: items.map((iItem) => transformItemToRESTPayload(iItem, id, master, config)),
|
|
64
|
+
items: items.map((iItem) => transformItemToRESTPayload(iItem, id, master, config, itemAttachedToMenu)),
|
|
64
65
|
};
|
|
65
66
|
};
|
|
66
67
|
|
|
@@ -251,6 +252,9 @@ export const prepareItemToViewPayload = (items = [], viewParentId = null, config
|
|
|
251
252
|
}));
|
|
252
253
|
|
|
253
254
|
export const extractRelatedItemLabel = (item = {}, fields = {}, config = {}) => {
|
|
255
|
+
if (get(item, 'isSingle', false)) {
|
|
256
|
+
return get(item, 'labelSingular', '');
|
|
257
|
+
}
|
|
254
258
|
const { contentTypes = [] } = config;
|
|
255
259
|
const { __collectionUid } = item;
|
|
256
260
|
const contentType = contentTypes.find(_ => _.uid === __collectionUid)
|
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
"header.action.newItem": "New Item",
|
|
6
6
|
"submit.cta.cancel": "Cancel",
|
|
7
7
|
"submit.cta.save": "Save",
|
|
8
|
-
"empty": "
|
|
8
|
+
"empty": "Your navigation is empty",
|
|
9
9
|
"empty.cta": "Create first item",
|
|
10
|
-
"popup.item.header": "
|
|
10
|
+
"popup.item.header.edit": "Edit navigation item",
|
|
11
|
+
"popup.item.header.new": "New navigation item",
|
|
11
12
|
"popup.item.form.title.label": "Title",
|
|
12
13
|
"popup.item.form.title.placeholder": "Enter the item title or leave blank to pull from related entity",
|
|
13
14
|
"popup.item.form.uiRouterKey.label": "UI router key",
|
|
@@ -28,6 +29,7 @@
|
|
|
28
29
|
"popup.item.form.relatedSection.label": "Relation to",
|
|
29
30
|
"popup.item.form.relatedType.label": "Content Type",
|
|
30
31
|
"popup.item.form.relatedType.placeholder": "Select content type...",
|
|
32
|
+
"popup.item.form.relatedType.empty": "There are no content types to select",
|
|
31
33
|
"popup.item.form.related.label": "Entity",
|
|
32
34
|
"popup.item.form.related.empty": "There are no more entities of \"{ contentTypeName }\" to select",
|
|
33
35
|
"popup.item.form.button.create": "Create item",
|
|
@@ -41,7 +43,7 @@
|
|
|
41
43
|
"notification.navigation.item.relation": "Entity relation does not exist!",
|
|
42
44
|
"notification.navigation.item.relation.status.draft": "draft",
|
|
43
45
|
"notification.navigation.item.relation.status.published": "published",
|
|
44
|
-
"navigation.item.action.newItem": "
|
|
46
|
+
"navigation.item.action.newItem": "Add nested item",
|
|
45
47
|
"navigation.item.badge.removed": "Removed",
|
|
46
48
|
"navigation.item.badge.draft": "{type}: Draft",
|
|
47
49
|
"navigation.item.badge.published": "{type}: Published"
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "strapi-plugin-navigation",
|
|
3
|
-
"version": "2.0.0
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Strapi - Navigation plugin",
|
|
5
5
|
"strapi": {
|
|
6
6
|
"name": "navigation",
|
|
7
|
-
"
|
|
8
|
-
"description": "
|
|
7
|
+
"displayName": "Navigation",
|
|
8
|
+
"description": "Create consumable navigation with a simple and straightforward visual builder",
|
|
9
9
|
"kind": "plugin"
|
|
10
10
|
},
|
|
11
11
|
"repository": {
|
|
@@ -13,10 +13,11 @@
|
|
|
13
13
|
"url": "https://github.com/VirtusLab/strapi-plugin-navigation"
|
|
14
14
|
},
|
|
15
15
|
"scripts": {
|
|
16
|
+
"publish": "npm publish --tag latest",
|
|
16
17
|
"test:unit": "jest --verbose --coverage"
|
|
17
18
|
},
|
|
18
19
|
"dependencies": {
|
|
19
|
-
"@strapi/utils": "^4.0.
|
|
20
|
+
"@strapi/utils": "^4.0.7",
|
|
20
21
|
"uuid": "^8.3.0",
|
|
21
22
|
"bad-words": "^3.0.3",
|
|
22
23
|
"lodash": "^4.17.11",
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"collectionName": "navigations",
|
|
3
|
+
"info": {
|
|
4
|
+
"singularName": "navigation",
|
|
5
|
+
"pluralName": "navigations",
|
|
6
|
+
"displayName": "Navigation",
|
|
7
|
+
"name": "navigation"
|
|
8
|
+
},
|
|
9
|
+
"options": {
|
|
10
|
+
"increments": true,
|
|
11
|
+
"comment": ""
|
|
12
|
+
},
|
|
13
|
+
"pluginOptions": {
|
|
14
|
+
"content-manager": {
|
|
15
|
+
"visible": true
|
|
16
|
+
},
|
|
17
|
+
"content-type-builder": {
|
|
18
|
+
"visible": false
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"attributes": {
|
|
22
|
+
"name": {
|
|
23
|
+
"type": "text",
|
|
24
|
+
"configurable": false,
|
|
25
|
+
"required": true
|
|
26
|
+
},
|
|
27
|
+
"slug": {
|
|
28
|
+
"type": "uid",
|
|
29
|
+
"target": "name",
|
|
30
|
+
"configurable": false,
|
|
31
|
+
"required": true
|
|
32
|
+
},
|
|
33
|
+
"visible": {
|
|
34
|
+
"type": "boolean",
|
|
35
|
+
"default": false,
|
|
36
|
+
"configurable": false
|
|
37
|
+
},
|
|
38
|
+
"items": {
|
|
39
|
+
"type": "relation",
|
|
40
|
+
"relation": "oneToMany",
|
|
41
|
+
"target": "plugin::navigation.navigation-item",
|
|
42
|
+
"configurable": false
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -1,84 +1,69 @@
|
|
|
1
|
-
const { setupStrapi } = require('
|
|
1
|
+
const { setupStrapi } = require('../../../__mocks__/strapi');
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
describe('Navigation services', () => {
|
|
4
|
+
beforeAll(async () => {
|
|
5
|
+
setupStrapi();
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
describe('Correct config', () => {
|
|
9
|
+
it('Declares Strapi instance', () => {
|
|
10
|
+
expect(strapi).toBeDefined()
|
|
11
|
+
expect(strapi.plugin('navigation').service('navigation')).toBeDefined()
|
|
12
|
+
});
|
|
4
13
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
14
|
+
it('Defines proper content types', () => {
|
|
15
|
+
expect(strapi.contentTypes).toBeDefined()
|
|
16
|
+
expect(strapi.plugin('navigation').contentTypes).toBeDefined()
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('Can read and return plugins config', () => {
|
|
20
|
+
expect(strapi.plugin('navigation').config('contentTypes')).toBeDefined()
|
|
21
|
+
expect(strapi.plugin('navigation').config('allowedLevels')).toBeDefined()
|
|
22
|
+
expect(strapi.plugin('navigation').config()).not.toBeDefined()
|
|
23
|
+
});
|
|
10
24
|
});
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
visible: true,
|
|
54
|
-
}, {
|
|
55
|
-
uid: 'plugins::another-plugin.pages',
|
|
56
|
-
collectionName: 'pages',
|
|
57
|
-
isSingle: false,
|
|
58
|
-
contentTypeName: 'Plugin-pages',
|
|
59
|
-
endpoint: 'plugin-pages',
|
|
60
|
-
label: 'Pages',
|
|
61
|
-
labelSingular: 'Page',
|
|
62
|
-
name: 'plugin-page',
|
|
63
|
-
visible: true,
|
|
64
|
-
plugin: 'another-plugin',
|
|
65
|
-
}, {
|
|
66
|
-
uid: 'plugins::another-plugin.blog-post',
|
|
67
|
-
collectionName: 'blog_posts',
|
|
68
|
-
isSingle: false,
|
|
69
|
-
contentTypeName: 'BlogPost',
|
|
70
|
-
endpoint: 'plugin-blog-posts',
|
|
71
|
-
label: 'Blog posts',
|
|
72
|
-
labelSingular: 'Blog post',
|
|
73
|
-
name: 'plugin-blog-post',
|
|
74
|
-
visible: true,
|
|
75
|
-
plugin: 'another-plugin',
|
|
76
|
-
}];
|
|
77
|
-
return configContentTypes().then(types => {
|
|
78
|
-
types.map(type => {
|
|
79
|
-
const result = results.find(({ uid }) => uid === type.uid);
|
|
80
|
-
expect(type).toMatchObject(result);
|
|
81
|
-
});
|
|
25
|
+
|
|
26
|
+
describe('Render navigation', () => {
|
|
27
|
+
it('Can render branch in flat format', async () => {
|
|
28
|
+
const service = strapi.plugin('navigation').service('navigation');
|
|
29
|
+
const result = await service.render(1);
|
|
30
|
+
|
|
31
|
+
expect(result).toBeDefined()
|
|
32
|
+
expect(result.length).toBe(2)
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('Can render branch in tree format', async () => {
|
|
36
|
+
const service = strapi.plugin('navigation').service('navigation');
|
|
37
|
+
const result = await service.render(1, "TREE");
|
|
38
|
+
|
|
39
|
+
expect(result).toBeDefined()
|
|
40
|
+
expect(result.length).toBeGreaterThan(0)
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('Can render branch in rfr format', async () => {
|
|
44
|
+
const service = strapi.plugin('navigation').service('navigation');
|
|
45
|
+
const result = await service.render(1, "RFR");
|
|
46
|
+
|
|
47
|
+
expect(result).toBeDefined()
|
|
48
|
+
expect(result.length).toBeGreaterThan(0)
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('Can render only menu attached elements', async () => {
|
|
52
|
+
const service = strapi.plugin('navigation').service('navigation');
|
|
53
|
+
const result = await service.render(1, "FLAT", true);
|
|
54
|
+
|
|
55
|
+
expect(result).toBeDefined()
|
|
56
|
+
expect(result.length).toBe(1)
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('Render child', () => {
|
|
61
|
+
it('Can render child', async () => {
|
|
62
|
+
const service = strapi.plugin('navigation').service('navigation');
|
|
63
|
+
const result = await service.renderChildren(1, "home");
|
|
64
|
+
|
|
65
|
+
expect(result).toBeDefined();
|
|
66
|
+
expect(result.length).toBe(1);
|
|
82
67
|
});
|
|
83
68
|
});
|
|
84
69
|
});
|
|
@@ -67,14 +67,14 @@ module.exports = ({ strapi }) => {
|
|
|
67
67
|
|
|
68
68
|
// Get plugin config
|
|
69
69
|
async config() {
|
|
70
|
-
const { pluginName, audienceModel } = utilsFunctions.extractMeta(strapi.plugins);
|
|
70
|
+
const { pluginName, audienceModel, service } = utilsFunctions.extractMeta(strapi.plugins);
|
|
71
71
|
const additionalFields = strapi.plugin(pluginName).config('additionalFields')
|
|
72
72
|
const contentTypesNameFields = strapi.plugin(pluginName).config('contentTypesNameFields');
|
|
73
73
|
const allowedLevels = strapi.plugin(pluginName).config('allowedLevels');
|
|
74
74
|
|
|
75
75
|
let extendedResult = {};
|
|
76
76
|
const result = {
|
|
77
|
-
contentTypes: await
|
|
77
|
+
contentTypes: await service.configContentTypes(),
|
|
78
78
|
contentTypesNameFields: {
|
|
79
79
|
default: contentTypesNameFieldsDefaults,
|
|
80
80
|
...(isObject(contentTypesNameFields) ? contentTypesNameFields : {}),
|
|
@@ -85,7 +85,7 @@ module.exports = ({ strapi }) => {
|
|
|
85
85
|
|
|
86
86
|
if (additionalFields.includes(configAdditionalFields.AUDIENCE)) {
|
|
87
87
|
const audienceItems = await strapi
|
|
88
|
-
.query(
|
|
88
|
+
.query(audienceModel.uid)
|
|
89
89
|
.findMany({
|
|
90
90
|
paggination: {
|
|
91
91
|
limit: -1,
|
|
@@ -20,19 +20,12 @@ module.exports = ({ strapi }) => {
|
|
|
20
20
|
|
|
21
21
|
extractMeta(plugins) {
|
|
22
22
|
const { navigation: plugin } = plugins;
|
|
23
|
-
const { navigation: service } = plugin.services;
|
|
24
|
-
const {
|
|
25
|
-
navigation: masterModel,
|
|
26
|
-
'navigation-item': itemModel,
|
|
27
|
-
audience: audienceModel,
|
|
28
|
-
'navigations-items-related': relatedModel,
|
|
29
|
-
} = plugin.contentTypes;
|
|
30
23
|
return {
|
|
31
|
-
masterModel,
|
|
32
|
-
itemModel,
|
|
33
|
-
relatedModel,
|
|
34
|
-
audienceModel,
|
|
35
|
-
service,
|
|
24
|
+
masterModel: plugin.contentType('navigation'),
|
|
25
|
+
itemModel: plugin.contentType('navigation-item'),
|
|
26
|
+
relatedModel: plugin.contentType('navigations-items-related'),
|
|
27
|
+
audienceModel: plugin.contentType('audience'),
|
|
28
|
+
service: plugin.service('navigation'),
|
|
36
29
|
plugin,
|
|
37
30
|
pluginName: 'navigation',
|
|
38
31
|
};
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
module.exports = {
|
|
2
|
-
collectionName: "navigations",
|
|
3
|
-
info: {
|
|
4
|
-
singularName: "navigation",
|
|
5
|
-
pluralName: "navigations",
|
|
6
|
-
displayName: "Navigation",
|
|
7
|
-
name: "navigation"
|
|
8
|
-
},
|
|
9
|
-
options: {
|
|
10
|
-
increments: true,
|
|
11
|
-
comment: ""
|
|
12
|
-
},
|
|
13
|
-
pluginOptions: {
|
|
14
|
-
"content-manager": {
|
|
15
|
-
visible: true
|
|
16
|
-
},
|
|
17
|
-
"content-type-builder": {
|
|
18
|
-
visible: false
|
|
19
|
-
}
|
|
20
|
-
},
|
|
21
|
-
attributes: {
|
|
22
|
-
name: {
|
|
23
|
-
type: "text",
|
|
24
|
-
configurable: false,
|
|
25
|
-
required: true
|
|
26
|
-
},
|
|
27
|
-
slug: {
|
|
28
|
-
type: "uid",
|
|
29
|
-
target: "name",
|
|
30
|
-
configurable: false,
|
|
31
|
-
required: true
|
|
32
|
-
},
|
|
33
|
-
visible: {
|
|
34
|
-
type: "boolean",
|
|
35
|
-
default: false,
|
|
36
|
-
configurable: false
|
|
37
|
-
},
|
|
38
|
-
items: {
|
|
39
|
-
type: "relation",
|
|
40
|
-
relation: "oneToMany",
|
|
41
|
-
target: "plugin::navigation.navigation-item",
|
|
42
|
-
configurable: false
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|