strapi-plugin-navigation 2.0.6 → 2.0.7
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 +1 -0
- package/admin/src/components/CollapseButton/index.js +31 -0
- package/admin/src/components/Item/ItemCardBadge/index.js +4 -4
- package/admin/src/components/Item/ItemCardHeader/Wrapper.js +0 -4
- package/admin/src/components/Item/ItemCardHeader/index.js +4 -6
- package/admin/src/components/Item/Wrapper.js +1 -1
- package/admin/src/components/Item/index.js +48 -40
- package/admin/src/components/NavigationItemList/Wrapper.js +1 -1
- package/admin/src/components/NavigationItemList/index.js +3 -0
- package/admin/src/pages/SettingsPage/index.js +35 -15
- package/admin/src/pages/View/components/NavigationItemForm/index.js +47 -30
- package/admin/src/pages/View/index.js +73 -9
- package/admin/src/pages/View/utils/enums.js +1 -0
- package/admin/src/pages/View/utils/parsers.js +6 -3
- package/admin/src/translations/en.json +12 -3
- package/package.json +1 -1
- package/server/bootstrap.js +3 -22
- package/server/config/index.js +1 -0
- package/server/config.js +1 -0
- package/server/content-types/navigation-item/lifecycle.js +1 -0
- package/server/content-types/navigation-item/schema.json +7 -1
- package/server/graphql/types/content-types-name-fields.js +2 -2
- package/server/services/navigation.js +44 -19
- package/server/services/utils/functions.js +1 -1
package/README.md
CHANGED
|
@@ -41,6 +41,7 @@ Strapi Navigation Plugin provides a website navigation / menu builder feature fo
|
|
|
41
41
|
- **Navigation Public API:** Simple and ready for use API endpoint for consuming the navigation structure you've created
|
|
42
42
|
- **Visual builder:** Elegant and easy to use visual builder
|
|
43
43
|
- **Any Content Type relation:** Navigation can by linked to any of your Content Types by default. Simply, you're controlling it and also limiting available content types by configuration props
|
|
44
|
+
- **Different types of navigation items:** Create navigation with items linked to internal types, to external links or wrapper elements to keep structure clean
|
|
44
45
|
- **Multiple navigations:** Create as many Navigation containers as you want, setup them and use in the consumer application
|
|
45
46
|
- **Customizable:** Possibility to customize the options like: available Content Types, Maximum level for "attach to menu", Additional fields (audience)
|
|
46
47
|
- **[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
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import styled from 'styled-components'
|
|
3
|
+
import { Flex } from '@strapi/design-system/Flex';
|
|
4
|
+
import { Typography } from '@strapi/design-system/Typography';
|
|
5
|
+
import { Icon } from '@strapi/design-system/Icon';
|
|
6
|
+
import { CarretUp, CarretDown } from '@strapi/icons';
|
|
7
|
+
|
|
8
|
+
const Wrapper = styled.div`
|
|
9
|
+
border-radius: 50%;
|
|
10
|
+
background: #DCDCE4;
|
|
11
|
+
width: 25px;
|
|
12
|
+
height: 25px;
|
|
13
|
+
display: flex;
|
|
14
|
+
justify-content: center;
|
|
15
|
+
align-items: center;
|
|
16
|
+
margin-right: 8px;
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
const CollapseButton = ({ toggle, collapsed, itemsCount }) => (
|
|
20
|
+
<Flex justifyContent='space-between' alignItems='center' onClick={toggle} cursor="pointer" style={{ marginRight: '16px' }}>
|
|
21
|
+
<Wrapper>
|
|
22
|
+
{ collapsed ?
|
|
23
|
+
<Icon as={CarretDown} width='7px' height='4px' /> :
|
|
24
|
+
<Icon as={CarretUp} width='7px' height='4px' />
|
|
25
|
+
}
|
|
26
|
+
</Wrapper>
|
|
27
|
+
<Typography variant="pi">{itemsCount} nested items</Typography>
|
|
28
|
+
</Flex >
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
export default CollapseButton;
|
|
@@ -4,15 +4,15 @@ import { Badge } from '@strapi/design-system/Badge';
|
|
|
4
4
|
const ItemCardBadge = styled(Badge)`
|
|
5
5
|
border: 1px solid ${({ theme, borderColor }) => theme.colors[borderColor]};
|
|
6
6
|
|
|
7
|
-
${
|
|
8
|
-
padding: ${
|
|
9
|
-
margin: 0px ${
|
|
7
|
+
${ ({small, theme}) => small && `
|
|
8
|
+
padding: ${theme.spaces[1]} ${theme.spaces[2]};
|
|
9
|
+
margin: 0px ${theme.spaces[3]};
|
|
10
10
|
vertical-align: middle;
|
|
11
11
|
|
|
12
12
|
cursor: default;
|
|
13
13
|
|
|
14
14
|
span {
|
|
15
|
-
font-size: .
|
|
15
|
+
font-size: .65rem;
|
|
16
16
|
line-height: 1;
|
|
17
17
|
vertical-align: middle;
|
|
18
18
|
}
|
|
@@ -9,10 +9,6 @@ const CardItemTitle = styled(CardTitle)`
|
|
|
9
9
|
justify-content: space-between;
|
|
10
10
|
align-items: center;
|
|
11
11
|
|
|
12
|
-
color: ${({ theme }) => theme.colors.neutral800};
|
|
13
|
-
font-size: ${({ theme }) => theme.fontSizes[2]};
|
|
14
|
-
font-weight: ${({ theme }) => theme.fontWeights.bold};
|
|
15
|
-
|
|
16
12
|
> div > * {
|
|
17
13
|
margin: 0px ${({ theme }) => theme.spaces[1]};
|
|
18
14
|
}
|
|
@@ -14,16 +14,17 @@ const ItemCardHeader = ({ title, path, icon, removed, onItemRemove, onItemEdit,
|
|
|
14
14
|
return (
|
|
15
15
|
<Wrapper>
|
|
16
16
|
<Flex alignItems="center">
|
|
17
|
-
<
|
|
17
|
+
<IconButton ref={dragRef} label="Drag" icon={<Drag />} />
|
|
18
18
|
<Typography variant="omega" fontWeight="bold">
|
|
19
19
|
{title}
|
|
20
20
|
</Typography>
|
|
21
21
|
<Typography variant="omega" fontWeight="bold" textColor='neutral500'>
|
|
22
22
|
{path}
|
|
23
23
|
</Typography>
|
|
24
|
+
<Icon as={icon}/>
|
|
24
25
|
</Flex>
|
|
25
26
|
<Flex alignItems="center" style={{ zIndex: 2 }}>
|
|
26
|
-
{removed &&
|
|
27
|
+
{removed &&
|
|
27
28
|
(<ItemCardBadge
|
|
28
29
|
borderColor={`danger200`}
|
|
29
30
|
backgroundColor={`danger100`}
|
|
@@ -36,10 +37,7 @@ const ItemCardHeader = ({ title, path, icon, removed, onItemRemove, onItemEdit,
|
|
|
36
37
|
<IconButton disabled={removed} onClick={onItemEdit} label="Edit" icon={<Pencil />} />
|
|
37
38
|
{removed ?
|
|
38
39
|
<IconButton onClick={onItemRestore} label="Restore" icon={<Refresh />} /> :
|
|
39
|
-
|
|
40
|
-
<IconButton ref={dragRef} label="Drag" icon={<Drag />} />
|
|
41
|
-
<IconButton onClick={onItemRemove} label="Remove" icon={<Trash />} />
|
|
42
|
-
</>
|
|
40
|
+
<IconButton onClick={onItemRemove} label="Remove" icon={<Trash />} />
|
|
43
41
|
}
|
|
44
42
|
</Flex>
|
|
45
43
|
</Wrapper>
|
|
@@ -3,7 +3,7 @@ import styled from "styled-components";
|
|
|
3
3
|
const Wrapper = styled.div`
|
|
4
4
|
position: relative;
|
|
5
5
|
margin-top: ${({theme}) => theme.spaces[2]};
|
|
6
|
-
margin-left: ${({
|
|
6
|
+
margin-left: ${({ level }) => level && '54px'}};
|
|
7
7
|
|
|
8
8
|
${({ level, theme, isLast }) => level && `
|
|
9
9
|
&::before {
|
|
@@ -11,7 +11,7 @@ import { Flex } from '@strapi/design-system/Flex';
|
|
|
11
11
|
import { Link } from '@strapi/design-system/Link';
|
|
12
12
|
import { TextButton } from '@strapi/design-system/TextButton';
|
|
13
13
|
import { Typography } from '@strapi/design-system/Typography';
|
|
14
|
-
import { ArrowRight, Link as LinkIcon, Earth, Plus } from '@strapi/icons';
|
|
14
|
+
import { ArrowRight, Link as LinkIcon, Earth, Plus, Cog } from '@strapi/icons';
|
|
15
15
|
|
|
16
16
|
import { navigationItemType } from '../../pages/View/utils/enums';
|
|
17
17
|
import ItemCardHeader from './ItemCardHeader';
|
|
@@ -21,6 +21,7 @@ import { extractRelatedItemLabel } from '../../pages/View/utils/parsers';
|
|
|
21
21
|
import ItemCardBadge from './ItemCardBadge';
|
|
22
22
|
import { ItemCardRemovedOverlay } from './ItemCardRemovedOverlay';
|
|
23
23
|
import { getMessage, ItemTypes } from '../../utils';
|
|
24
|
+
import CollapseButton from '../CollapseButton';
|
|
24
25
|
|
|
25
26
|
const Item = (props) => {
|
|
26
27
|
const {
|
|
@@ -36,6 +37,7 @@ const Item = (props) => {
|
|
|
36
37
|
onItemRestore,
|
|
37
38
|
onItemEdit,
|
|
38
39
|
onItemReOrder,
|
|
40
|
+
onItemToggleCollapse,
|
|
39
41
|
error,
|
|
40
42
|
displayChildren,
|
|
41
43
|
config = {},
|
|
@@ -49,10 +51,12 @@ const Item = (props) => {
|
|
|
49
51
|
removed,
|
|
50
52
|
externalPath,
|
|
51
53
|
menuAttached,
|
|
54
|
+
collapsed,
|
|
52
55
|
} = item;
|
|
53
56
|
|
|
54
57
|
const { contentTypes, contentTypesNameFields } = config;
|
|
55
58
|
const isExternal = type === navigationItemType.EXTERNAL;
|
|
59
|
+
const isWrapper = type === navigationItemType.WRAPPER;
|
|
56
60
|
const isPublished = relatedRef && relatedRef?.publishedAt;
|
|
57
61
|
const isNextMenuAllowedLevel = isNumber(allowedLevels) ? level < (allowedLevels - 1) : true;
|
|
58
62
|
const isMenuAllowedLevel = isNumber(allowedLevels) ? level < allowedLevels : true;
|
|
@@ -97,7 +101,7 @@ const Item = (props) => {
|
|
|
97
101
|
const [{ isDragging }, drag, dragPreview] = useDrag({
|
|
98
102
|
type: `${ItemTypes.NAVIGATION_ITEM}_${levelPath}`,
|
|
99
103
|
item: () => {
|
|
100
|
-
return { ...item };
|
|
104
|
+
return { ...item, relatedRef };
|
|
101
105
|
},
|
|
102
106
|
collect: monitor => ({
|
|
103
107
|
isDragging: monitor.isDragging(),
|
|
@@ -119,7 +123,7 @@ const Item = (props) => {
|
|
|
119
123
|
<ItemCardHeader
|
|
120
124
|
title={title}
|
|
121
125
|
path={isExternal ? externalPath : absolutePath}
|
|
122
|
-
icon={isExternal ? Earth : LinkIcon}
|
|
126
|
+
icon={isExternal ? Earth : isWrapper ? Cog : LinkIcon}
|
|
123
127
|
onItemRemove={() => onItemRemove({
|
|
124
128
|
...item,
|
|
125
129
|
relatedRef,
|
|
@@ -128,6 +132,7 @@ const Item = (props) => {
|
|
|
128
132
|
...item,
|
|
129
133
|
isMenuAllowedLevel,
|
|
130
134
|
isParentAttachedToMenu,
|
|
135
|
+
relatedRef,
|
|
131
136
|
}, levelPath, isParentAttachedToMenu)}
|
|
132
137
|
onItemRestore={() => onItemRestore({
|
|
133
138
|
...item,
|
|
@@ -138,49 +143,50 @@ const Item = (props) => {
|
|
|
138
143
|
/>
|
|
139
144
|
</CardBody>
|
|
140
145
|
<Divider />
|
|
141
|
-
{!isExternal && (
|
|
142
|
-
<
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
146
|
+
{!isExternal && (
|
|
147
|
+
<CardBody style={{ padding: '8px' }}>
|
|
148
|
+
<Flex style={{ width: '100%' }} direction="row" alignItems="center" justifyContent="space-between">
|
|
149
|
+
<Flex>
|
|
150
|
+
{!isEmpty(item.items) && <CollapseButton toggle={() => onItemToggleCollapse({...item, relatedRef})} collapsed={collapsed} itemsCount={item.items.length}/>}
|
|
151
|
+
<TextButton
|
|
152
|
+
disabled={removed}
|
|
153
|
+
startIcon={<Plus />}
|
|
154
|
+
onClick={(e) => onItemLevelAdd(e, viewId, isNextMenuAllowedLevel, absolutePath, menuAttached)}
|
|
155
|
+
>
|
|
156
|
+
<Typography variant="pi" fontWeight="bold" textColor={removed ? "neutral600" : "primary600"}>
|
|
157
|
+
{getMessage("components.navigationItem.action.newItem")}
|
|
158
|
+
</Typography>
|
|
159
|
+
</TextButton>
|
|
160
|
+
</Flex>
|
|
161
|
+
{relatedItemLabel && (
|
|
162
|
+
<Flex justifyContent='center' alignItems='center'>
|
|
163
|
+
<ItemCardBadge
|
|
164
|
+
borderColor={`${relatedBadgeColor}200`}
|
|
165
|
+
backgroundColor={`${relatedBadgeColor}100`}
|
|
166
|
+
textColor={`${relatedBadgeColor}600`}
|
|
167
|
+
className="action"
|
|
168
|
+
small
|
|
169
|
+
>
|
|
170
|
+
{getMessage({id: `components.navigationItem.badge.${isPublished ? 'published' : 'draft'}`})}
|
|
171
|
+
</ItemCardBadge>
|
|
172
|
+
<Typography variant="omega" textColor='neutral600'>{relatedTypeLabel} / </Typography>
|
|
173
|
+
<Typography variant="omega" textColor='neutral800'>{relatedItemLabel}</Typography>
|
|
174
|
+
<Link
|
|
175
|
+
to={`/content-manager/collectionType/${relatedRef?.__collectionUid}/${relatedRef?.id}`}
|
|
176
|
+
endIcon={<ArrowRight />}> </Link>
|
|
177
|
+
</Flex>)
|
|
178
|
+
}
|
|
179
|
+
</Flex>
|
|
180
|
+
</CardBody>)}
|
|
176
181
|
</div>
|
|
177
182
|
</Card>
|
|
178
|
-
{hasChildren && !removed && <List
|
|
183
|
+
{hasChildren && !removed && !collapsed && <List
|
|
179
184
|
onItemLevelAdd={onItemLevelAdd}
|
|
180
185
|
onItemRemove={onItemRemove}
|
|
181
186
|
onItemEdit={onItemEdit}
|
|
182
187
|
onItemRestore={onItemRestore}
|
|
183
188
|
onItemReOrder={onItemReOrder}
|
|
189
|
+
onItemToggleCollapse={onItemToggleCollapse}
|
|
184
190
|
error={error}
|
|
185
191
|
allowedLevels={allowedLevels}
|
|
186
192
|
isParentAttachedToMenu={menuAttached}
|
|
@@ -204,7 +210,8 @@ Item.propTypes = {
|
|
|
204
210
|
path: PropTypes.string,
|
|
205
211
|
externalPath: PropTypes.string,
|
|
206
212
|
related: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
207
|
-
menuAttached: PropTypes.bool
|
|
213
|
+
menuAttached: PropTypes.bool,
|
|
214
|
+
collapsed: PropTypes.bool,
|
|
208
215
|
}).isRequired,
|
|
209
216
|
relatedRef: PropTypes.object,
|
|
210
217
|
level: PropTypes.number,
|
|
@@ -214,6 +221,7 @@ Item.propTypes = {
|
|
|
214
221
|
onItemLevelAdd: PropTypes.func.isRequired,
|
|
215
222
|
onItemRemove: PropTypes.func.isRequired,
|
|
216
223
|
onItemReOrder: PropTypes.func.isRequired,
|
|
224
|
+
onItemToggleCollapse: PropTypes.func.isRequired,
|
|
217
225
|
config: PropTypes.shape({
|
|
218
226
|
contentTypes: PropTypes.array.isRequired,
|
|
219
227
|
contentTypesNameFields: PropTypes.object.isRequired,
|
|
@@ -16,6 +16,7 @@ const List = ({
|
|
|
16
16
|
onItemRemove,
|
|
17
17
|
onItemRestore,
|
|
18
18
|
onItemReOrder,
|
|
19
|
+
onItemToggleCollapse,
|
|
19
20
|
displayFlat,
|
|
20
21
|
contentTypes,
|
|
21
22
|
contentTypesNameFields,
|
|
@@ -38,6 +39,7 @@ const List = ({
|
|
|
38
39
|
onItemRemove={onItemRemove}
|
|
39
40
|
onItemEdit={onItemEdit}
|
|
40
41
|
onItemReOrder={onItemReOrder}
|
|
42
|
+
onItemToggleCollapse={onItemToggleCollapse}
|
|
41
43
|
error={error}
|
|
42
44
|
displayChildren={displayFlat}
|
|
43
45
|
config={{
|
|
@@ -60,6 +62,7 @@ List.propTypes = {
|
|
|
60
62
|
onItemRestore: PropTypes.func.isRequired,
|
|
61
63
|
onItemRestore: PropTypes.func.isRequired,
|
|
62
64
|
onItemReOrder: PropTypes.func.isRequired,
|
|
65
|
+
onItemToggleCollapse: PropTypes.func.isRequired,
|
|
63
66
|
contentTypes: PropTypes.array.isRequired,
|
|
64
67
|
contentTypesNameFields: PropTypes.object.isRequired
|
|
65
68
|
};
|
|
@@ -49,9 +49,10 @@ const SettingsPage = () => {
|
|
|
49
49
|
padding: 6,
|
|
50
50
|
};
|
|
51
51
|
|
|
52
|
-
const preparePayload = ({ selectedContentTypes, nameFields, audienceFieldChecked, allowedLevels }) => ({
|
|
52
|
+
const preparePayload = ({ selectedContentTypes, nameFields, audienceFieldChecked, allowedLevels, populate }) => ({
|
|
53
53
|
contentTypes: selectedContentTypes,
|
|
54
54
|
contentTypesNameFields: nameFields,
|
|
55
|
+
contentTypesPopulate: populate,
|
|
55
56
|
additionalFields: audienceFieldChecked ? [navigationItemAdditionalFields.AUDIENCE] : [],
|
|
56
57
|
allowedLevels: allowedLevels,
|
|
57
58
|
gql: {
|
|
@@ -110,8 +111,9 @@ const SettingsPage = () => {
|
|
|
110
111
|
const allContentTypes = !isLoading && Object.values(allContentTypesData).filter(item => item.uid.includes('api::'));
|
|
111
112
|
const selectedContentTypes = navigationConfigData?.contentTypes.map(item => item.uid);
|
|
112
113
|
const audienceFieldChecked = navigationConfigData?.additionalFields.includes(navigationItemAdditionalFields.AUDIENCE);
|
|
113
|
-
const allowedLevels = navigationConfigData?.allowedLevels;
|
|
114
|
-
const nameFields = navigationConfigData?.contentTypesNameFields
|
|
114
|
+
const allowedLevels = navigationConfigData?.allowedLevels || 2;
|
|
115
|
+
const nameFields = navigationConfigData?.contentTypesNameFields || {}
|
|
116
|
+
const populate = navigationConfigData?.contentTypesPopulate || {}
|
|
115
117
|
|
|
116
118
|
return (
|
|
117
119
|
<>
|
|
@@ -125,6 +127,7 @@ const SettingsPage = () => {
|
|
|
125
127
|
audienceFieldChecked,
|
|
126
128
|
allowedLevels,
|
|
127
129
|
nameFields,
|
|
130
|
+
populate,
|
|
128
131
|
}}
|
|
129
132
|
onSubmit={onSave}
|
|
130
133
|
>
|
|
@@ -183,6 +186,7 @@ const SettingsPage = () => {
|
|
|
183
186
|
{orderBy(values.selectedContentTypes).map(uid => {
|
|
184
187
|
const { attributes, info: { displayName } } = allContentTypes.find(item => item.uid == uid);
|
|
185
188
|
const stringAttributes = Object.keys(attributes).filter(_ => attributes[_].type === 'string');
|
|
189
|
+
const relationAttributes = Object.keys(attributes).filter(_ => attributes[_].type === 'relation');
|
|
186
190
|
const key = `collectionSettings-${uid}`;
|
|
187
191
|
return (<Accordion
|
|
188
192
|
expanded={contentTypeExpanded === key}
|
|
@@ -197,20 +201,36 @@ const SettingsPage = () => {
|
|
|
197
201
|
<Select
|
|
198
202
|
name={`collectionSettings-${uid}-entryLabel`}
|
|
199
203
|
label={getMessage('pages.settings.form.nameField.label')}
|
|
200
|
-
hint={getMessage(
|
|
204
|
+
hint={getMessage(`pages.settings.form.populate.${isEmpty(stringAttributes) ? 'empty' : 'hint'}`)}
|
|
201
205
|
placeholder={getMessage('pages.settings.form.nameField.placeholder')}
|
|
202
206
|
onClear={() => null}
|
|
203
207
|
value={values.nameFields[uid] || []}
|
|
204
208
|
onChange={(value) => setFieldValue('nameFields', prepareNameFieldFor(uid, values.nameFields, value))}
|
|
205
209
|
multi
|
|
206
210
|
withTags
|
|
207
|
-
disabled={isRestartRequired}
|
|
211
|
+
disabled={isRestartRequired || isEmpty(stringAttributes)}
|
|
208
212
|
>
|
|
209
213
|
{stringAttributes.map(key =>
|
|
210
214
|
(<Option key={uid + key} value={key}>{capitalize(key.split('_').join(' '))}</Option>))}
|
|
215
|
+
</Select>
|
|
216
|
+
<Select
|
|
217
|
+
name={`collectionSettings-${uid}-populate`}
|
|
218
|
+
label={getMessage('pages.settings.form.populate.label')}
|
|
219
|
+
hint={getMessage(`pages.settings.form.populate.${isEmpty(relationAttributes) ? 'empty' : 'hint'}`)}
|
|
220
|
+
placeholder={getMessage('pages.settings.form.populate.placeholder')}
|
|
221
|
+
onClear={() => null}
|
|
222
|
+
value={values.populate[uid] || []}
|
|
223
|
+
onChange={(value) => setFieldValue('populate', prepareNameFieldFor(uid, values.populate, value))}
|
|
224
|
+
multi
|
|
225
|
+
withTags
|
|
226
|
+
disabled={isRestartRequired || isEmpty(relationAttributes)}
|
|
227
|
+
>
|
|
228
|
+
{relationAttributes.map(key =>
|
|
229
|
+
(<Option key={uid + key} value={key}>{capitalize(key.split('_').join(' '))}</Option>))}
|
|
211
230
|
</Select>
|
|
212
231
|
</Stack>
|
|
213
232
|
</Box>
|
|
233
|
+
|
|
214
234
|
</AccordionContent>
|
|
215
235
|
</Accordion>);
|
|
216
236
|
})}
|
|
@@ -226,16 +246,16 @@ const SettingsPage = () => {
|
|
|
226
246
|
</Typography>
|
|
227
247
|
<Grid gap={4}>
|
|
228
248
|
<GridItem col={3} s={6} xs={12}>
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
249
|
+
<NumberInput
|
|
250
|
+
name="allowedLevels"
|
|
251
|
+
label={getMessage('pages.settings.form.allowedLevels.label')}
|
|
252
|
+
placeholder={getMessage('pages.settings.form.allowedLevels.placeholder')}
|
|
253
|
+
hint={getMessage('pages.settings.form.allowedLevels.hint')}
|
|
254
|
+
onValueChange={(value) => setFieldValue('allowedLevels', value, false)}
|
|
255
|
+
value={values.allowedLevels}
|
|
256
|
+
disabled={isRestartRequired}
|
|
257
|
+
/>
|
|
258
|
+
</GridItem>
|
|
239
259
|
<GridItem col={6} s={12} xs={12}>
|
|
240
260
|
<ToggleInput
|
|
241
261
|
name="audienceFieldChecked"
|
|
@@ -2,6 +2,7 @@ import React, { useEffect, useMemo, useState, useCallback } from 'react';
|
|
|
2
2
|
import { debounce, find, get, isEmpty, isEqual, isNil, isString } from 'lodash';
|
|
3
3
|
import PropTypes from 'prop-types';
|
|
4
4
|
import { Formik } from 'formik'
|
|
5
|
+
import slugify from 'slugify';
|
|
5
6
|
|
|
6
7
|
// Design System
|
|
7
8
|
import { ModalBody } from '@strapi/design-system/ModalLayout';
|
|
@@ -10,10 +11,7 @@ import { Grid, GridItem } from '@strapi/design-system/Grid';
|
|
|
10
11
|
import { Form, GenericInput } from '@strapi/helper-plugin';
|
|
11
12
|
|
|
12
13
|
import { NavigationItemPopupFooter } from '../NavigationItemPopup/NavigationItemPopupFooter';
|
|
13
|
-
|
|
14
|
-
|
|
15
14
|
import { navigationItemAdditionalFields, navigationItemType } from '../../utils/enums';
|
|
16
|
-
import slugify from 'slugify';
|
|
17
15
|
import { extractRelatedItemLabel } from '../../utils/parsers';
|
|
18
16
|
import { form as formDefinition } from './utils/form';
|
|
19
17
|
import { checkFormValidity } from '../../utils/form';
|
|
@@ -76,20 +74,19 @@ const NavigationItemForm = ({
|
|
|
76
74
|
};
|
|
77
75
|
|
|
78
76
|
const sanitizePayload = (payload = {}) => {
|
|
79
|
-
const { onItemClick, onItemLevelAddClick, related, relatedType, menuAttached, ...purePayload } = payload;
|
|
80
|
-
const sanitizedType = purePayload.type || navigationItemType.INTERNAL;
|
|
77
|
+
const { onItemClick, onItemLevelAddClick, related, relatedType, menuAttached, type, ...purePayload } = payload;
|
|
81
78
|
const relatedId = related
|
|
82
79
|
const relatedCollectionType = relatedType;
|
|
83
80
|
const title = payload.title || relatedSelectOptions.find(v => v.key == relatedId)?.label
|
|
84
81
|
return {
|
|
85
82
|
...purePayload,
|
|
86
83
|
title,
|
|
84
|
+
type,
|
|
87
85
|
menuAttached: isNil(menuAttached) ? false : menuAttached,
|
|
88
|
-
type:
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
relatedType: relatedCollectionType,
|
|
86
|
+
path: type !== navigationItemType.EXTERNAL ? purePayload.path : undefined,
|
|
87
|
+
externalPath: type === navigationItemType.EXTERNAL ? purePayload.externalPath : undefined,
|
|
88
|
+
related: type === navigationItemType.INTERNAL ? relatedId : undefined,
|
|
89
|
+
relatedType: type === navigationItemType.INTERNAL ? relatedCollectionType : undefined,
|
|
93
90
|
isSingle: isSingleSelected,
|
|
94
91
|
uiRouterKey: generateUiRouterKey(purePayload.title, relatedId, relatedCollectionType),
|
|
95
92
|
};
|
|
@@ -109,11 +106,8 @@ const NavigationItemForm = ({
|
|
|
109
106
|
}
|
|
110
107
|
};
|
|
111
108
|
|
|
112
|
-
const onTypeChange = ({ target: { name, value } }) =>
|
|
113
|
-
onChange({ target: { name, value: value ? navigationItemType.INTERNAL : navigationItemType.EXTERNAL } });
|
|
114
|
-
|
|
115
109
|
const onAudienceChange = (value) => {
|
|
116
|
-
onChange({target: {name: `${inputsPrefix}audience`, value}});
|
|
110
|
+
onChange({ target: { name: `${inputsPrefix}audience`, value } });
|
|
117
111
|
}
|
|
118
112
|
|
|
119
113
|
const onChange = ({ target: { name, value } }) => {
|
|
@@ -148,6 +142,20 @@ const NavigationItemForm = ({
|
|
|
148
142
|
[relatedTypeSelectValue, contentTypes],
|
|
149
143
|
);
|
|
150
144
|
|
|
145
|
+
const navigationItemTypeOptions = Object.keys(navigationItemType).map(key => {
|
|
146
|
+
const value = navigationItemType[key].toLowerCase();
|
|
147
|
+
return {
|
|
148
|
+
key,
|
|
149
|
+
value: navigationItemType[key],
|
|
150
|
+
metadatas: {
|
|
151
|
+
intlLabel: {
|
|
152
|
+
id: getTradId(`popup.item.form.type.${value}.label`),
|
|
153
|
+
defaultMessage: getTradId(`popup.item.form.type.${value}.label`),
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
151
159
|
const relatedSelectOptions = contentTypeEntities
|
|
152
160
|
.filter((item) => {
|
|
153
161
|
const usedContentTypeEntitiesOfSameType = usedContentTypeEntities
|
|
@@ -178,7 +186,9 @@ const NavigationItemForm = ({
|
|
|
178
186
|
const isExternal = form.type === navigationItemType.EXTERNAL;
|
|
179
187
|
const pathSourceName = isExternal ? 'externalPath' : 'path';
|
|
180
188
|
|
|
181
|
-
const submitDisabled =
|
|
189
|
+
const submitDisabled =
|
|
190
|
+
(form.type === navigationItemType.INTERNAL && isNil(get(form, `${inputsPrefix}related`))) ||
|
|
191
|
+
(form.type === navigationItemType.WRAPPER && isNil(get(form, `${inputsPrefix}title`)));
|
|
182
192
|
|
|
183
193
|
const debouncedSearch = useCallback(
|
|
184
194
|
debounce(nextValue => setContentTypeSearchQuery(nextValue), 500),
|
|
@@ -285,7 +295,21 @@ const NavigationItemForm = ({
|
|
|
285
295
|
value={get(form, `${inputsPrefix}title`, '')}
|
|
286
296
|
/>
|
|
287
297
|
</GridItem>
|
|
288
|
-
<GridItem key={`${inputsPrefix}
|
|
298
|
+
<GridItem key={`${inputsPrefix}type`} col={4} lg={12}>
|
|
299
|
+
<GenericInput
|
|
300
|
+
intlLabel={{
|
|
301
|
+
id: getTradId('popup.item.form.type.label'),
|
|
302
|
+
defaultMessage: 'Internal link',
|
|
303
|
+
}}
|
|
304
|
+
name={`${inputsPrefix}type`}
|
|
305
|
+
options={navigationItemTypeOptions}
|
|
306
|
+
type='select'
|
|
307
|
+
error={get(formErrors, `${inputsPrefix}type.id`)}
|
|
308
|
+
onChange={onChange}
|
|
309
|
+
value={get(form, `${inputsPrefix}type`, '')}
|
|
310
|
+
/>
|
|
311
|
+
</GridItem>
|
|
312
|
+
<GridItem key={`${inputsPrefix}menuAttached`} col={4} lg={12}>
|
|
289
313
|
<GenericInput
|
|
290
314
|
intlLabel={{
|
|
291
315
|
id: getTradId('popup.item.form.menuAttached.label'),
|
|
@@ -299,19 +323,6 @@ const NavigationItemForm = ({
|
|
|
299
323
|
disabled={!(data.isMenuAllowedLevel && data.parentAttachedToMenu)}
|
|
300
324
|
/>
|
|
301
325
|
</GridItem>
|
|
302
|
-
<GridItem key={`${inputsPrefix}type`} col={6} lg={12}>
|
|
303
|
-
<GenericInput
|
|
304
|
-
intlLabel={{
|
|
305
|
-
id: getTradId('popup.item.form.type.label'),
|
|
306
|
-
defaultMessage: 'Internal link',
|
|
307
|
-
}}
|
|
308
|
-
name={`${inputsPrefix}type`}
|
|
309
|
-
type='bool'
|
|
310
|
-
error={get(formErrors, `${inputsPrefix}type.id`)}
|
|
311
|
-
onChange={onTypeChange}
|
|
312
|
-
value={get(form, `${inputsPrefix}type`, '') === navigationItemType.INTERNAL}
|
|
313
|
-
/>
|
|
314
|
-
</GridItem>
|
|
315
326
|
<GridItem key={`${inputsPrefix}path`} col={12}>
|
|
316
327
|
<GenericInput
|
|
317
328
|
intlLabel={{
|
|
@@ -330,7 +341,7 @@ const NavigationItemForm = ({
|
|
|
330
341
|
description={generatePreviewPath()}
|
|
331
342
|
/>
|
|
332
343
|
</GridItem>
|
|
333
|
-
{
|
|
344
|
+
{get(form, `${inputsPrefix}type`) === navigationItemType.INTERNAL && (
|
|
334
345
|
<>
|
|
335
346
|
<GridItem col={6} lg={12}>
|
|
336
347
|
<GenericInput
|
|
@@ -402,8 +413,14 @@ const NavigationItemForm = ({
|
|
|
402
413
|
label={getMessage('popup.item.form.audience.label')}
|
|
403
414
|
onChange={onAudienceChange}
|
|
404
415
|
value={audience}
|
|
416
|
+
hint={
|
|
417
|
+
!isLoading && isEmpty(audienceOptions)
|
|
418
|
+
? getMessage('popup.item.form.audience.empty', 'There are no more audiences')
|
|
419
|
+
: undefined
|
|
420
|
+
}
|
|
405
421
|
multi
|
|
406
422
|
withTags
|
|
423
|
+
disabled={isEmpty(audienceOptions)}
|
|
407
424
|
>
|
|
408
425
|
{audienceOptions.map(({ value, label }) => <Option key={value} value={value}>{label}</Option>)}
|
|
409
426
|
</Select>
|
|
@@ -120,6 +120,37 @@ const View = () => {
|
|
|
120
120
|
}, []);
|
|
121
121
|
const filteredList = !isSearchEmpty ? filteredListFactory(changedActiveNavigation.items, (item) => item?.title.includes(searchValue)) : [];
|
|
122
122
|
|
|
123
|
+
const changeCollapseItemDeep = (item, collapse) => {
|
|
124
|
+
if (item.collapsed !== collapse) {
|
|
125
|
+
return {
|
|
126
|
+
...item,
|
|
127
|
+
collapsed: collapse,
|
|
128
|
+
updated: true,
|
|
129
|
+
items: item.items?.map(el => changeCollapseItemDeep(el, collapse))
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
...item,
|
|
134
|
+
items: item.items?.map(el => changeCollapseItemDeep(el, collapse))
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const handleCollapseAll = () => {
|
|
139
|
+
handleChangeNavigationData({
|
|
140
|
+
...changedActiveNavigation,
|
|
141
|
+
items: changedActiveNavigation.items.map(item => changeCollapseItemDeep(item, true))
|
|
142
|
+
}, true);
|
|
143
|
+
setStructureChanged(true);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const handleExpandAll = () => {
|
|
147
|
+
handleChangeNavigationData({
|
|
148
|
+
...changedActiveNavigation,
|
|
149
|
+
items: changedActiveNavigation.items.map(item => changeCollapseItemDeep(item, false))
|
|
150
|
+
}, true);
|
|
151
|
+
setStructureChanged(true);
|
|
152
|
+
}
|
|
153
|
+
|
|
123
154
|
const handleItemReOrder = (item, newOrder) => {
|
|
124
155
|
handleSubmitNavigationItem({
|
|
125
156
|
...item,
|
|
@@ -141,6 +172,14 @@ const View = () => {
|
|
|
141
172
|
});
|
|
142
173
|
};
|
|
143
174
|
|
|
175
|
+
const handleItemToggleCollapse = (item) => {
|
|
176
|
+
handleSubmitNavigationItem({
|
|
177
|
+
...item,
|
|
178
|
+
collapsed: !item.collapsed,
|
|
179
|
+
updated: true,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
144
183
|
const handleItemEdit = (
|
|
145
184
|
item,
|
|
146
185
|
levelPath = '',
|
|
@@ -164,6 +203,33 @@ const View = () => {
|
|
|
164
203
|
setSearchValue('');
|
|
165
204
|
}
|
|
166
205
|
|
|
206
|
+
const endActions = [
|
|
207
|
+
{
|
|
208
|
+
onClick: handleExpandAll,
|
|
209
|
+
disabled: isLoadingForSubmit,
|
|
210
|
+
type: "submit",
|
|
211
|
+
variant: 'tertiary',
|
|
212
|
+
tradId: 'header.action.expandAll',
|
|
213
|
+
margin: '8px',
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
onClick: handleCollapseAll,
|
|
217
|
+
disabled: isLoadingForSubmit,
|
|
218
|
+
type: "submit",
|
|
219
|
+
variant: 'tertiary',
|
|
220
|
+
tradId: 'header.action.collapseAll',
|
|
221
|
+
margin: '8px',
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
onClick: addNewNavigationItem,
|
|
225
|
+
startIcon: <PlusIcon />,
|
|
226
|
+
disabled: isLoadingForSubmit,
|
|
227
|
+
type: "submit",
|
|
228
|
+
tradId: 'header.action.newItem',
|
|
229
|
+
margin: '16px',
|
|
230
|
+
},
|
|
231
|
+
]
|
|
232
|
+
|
|
167
233
|
return (
|
|
168
234
|
<Main labelledBy="title" aria-busy={isLoadingForSubmit}>
|
|
169
235
|
<NavigationHeader
|
|
@@ -180,18 +246,15 @@ const View = () => {
|
|
|
180
246
|
<>
|
|
181
247
|
<NavigationContentHeader
|
|
182
248
|
startActions={<Search value={searchValue} setValue={setSearchValue} />}
|
|
183
|
-
endActions={
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
>
|
|
189
|
-
{formatMessage(getTrad('header.action.newItem'))}
|
|
190
|
-
</Button>}
|
|
249
|
+
endActions={endActions.map(({ tradId, margin, ...item }, i) =>
|
|
250
|
+
<Box marginLeft={margin} key={i}>
|
|
251
|
+
<Button {...item}> {formatMessage(getTrad(tradId))} </Button>
|
|
252
|
+
</Box>
|
|
253
|
+
)}
|
|
191
254
|
/>
|
|
192
255
|
{isEmpty(changedActiveNavigation.items || []) && (
|
|
193
256
|
<Flex direction="column" minHeight="400px" justifyContent="center">
|
|
194
|
-
<Icon as={EmptyDocumentsIcon} width="160px" height="88px" color=""/>
|
|
257
|
+
<Icon as={EmptyDocumentsIcon} width="160px" height="88px" color="" />
|
|
195
258
|
<Box padding={4}>
|
|
196
259
|
<Typography variant="beta" textColor="neutral600">{formatMessage(getTrad('empty'))}</Typography>
|
|
197
260
|
</Box>
|
|
@@ -214,6 +277,7 @@ const View = () => {
|
|
|
214
277
|
onItemEdit={handleItemEdit}
|
|
215
278
|
onItemRestore={handleItemRestore}
|
|
216
279
|
onItemReOrder={handleItemReOrder}
|
|
280
|
+
onItemToggleCollapse={handleItemToggleCollapse}
|
|
217
281
|
displayFlat={!isSearchEmpty}
|
|
218
282
|
root
|
|
219
283
|
error={error}
|
|
@@ -24,12 +24,14 @@ export const transformItemToRESTPayload = (
|
|
|
24
24
|
order,
|
|
25
25
|
audience = [],
|
|
26
26
|
items = [],
|
|
27
|
+
collapsed,
|
|
27
28
|
} = item;
|
|
28
29
|
const isExternal = type === navigationItemType.EXTERNAL;
|
|
30
|
+
const isWrapper = type === navigationItemType.WRAPPER;
|
|
29
31
|
const { contentTypes = [] } = config;
|
|
30
32
|
|
|
31
33
|
const parsedRelated = Number(related);
|
|
32
|
-
const relatedId = isExternal || isNaN(parsedRelated) ? related?.value || related : parsedRelated;
|
|
34
|
+
const relatedId = isExternal || isWrapper || isNaN(parsedRelated) ? related?.value || related : parsedRelated;
|
|
33
35
|
|
|
34
36
|
const relatedContentType = relatedType ?
|
|
35
37
|
find(contentTypes,
|
|
@@ -46,13 +48,14 @@ export const transformItemToRESTPayload = (
|
|
|
46
48
|
removed,
|
|
47
49
|
order,
|
|
48
50
|
uiRouterKey,
|
|
51
|
+
collapsed,
|
|
49
52
|
menuAttached: itemAttachedToMenu,
|
|
50
53
|
audience: audience.map((audienceItem) =>
|
|
51
54
|
isObject(audienceItem) ? audienceItem.value : audienceItem,
|
|
52
55
|
),
|
|
53
56
|
path: isExternal ? undefined : path,
|
|
54
57
|
externalPath: isExternal ? externalPath : undefined,
|
|
55
|
-
related: isExternal
|
|
58
|
+
related: isExternal || isWrapper
|
|
56
59
|
? undefined
|
|
57
60
|
: [
|
|
58
61
|
{
|
|
@@ -274,7 +277,7 @@ export const usedContentTypes = (items = []) => items.flatMap(
|
|
|
274
277
|
|
|
275
278
|
export const isRelationCorrect = ({ related, type }) => {
|
|
276
279
|
const isRelationDefined = !isNil(related);
|
|
277
|
-
return type
|
|
280
|
+
return type !== navigationItemType.INTERNAL || (type === navigationItemType.INTERNAL && isRelationDefined);
|
|
278
281
|
};
|
|
279
282
|
|
|
280
283
|
export const isRelationPublished = ({ relatedRef, relatedType = {}, type, isCollection }) => {
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
"header.title": "Navigation",
|
|
4
4
|
"header.description": "Define your portal navigation",
|
|
5
5
|
"header.action.newItem": "New Item",
|
|
6
|
+
"header.action.collapseAll": "Collapse All",
|
|
7
|
+
"header.action.expandAll": "Expand All",
|
|
6
8
|
"submit.cta.cancel": "Cancel",
|
|
7
9
|
"submit.cta.save": "Save",
|
|
8
10
|
"empty": "Your navigation is empty",
|
|
@@ -20,12 +22,14 @@
|
|
|
20
22
|
"popup.item.form.externalPath.placeholder": "Link to the external source",
|
|
21
23
|
"popup.item.form.externalPath.validation.type": "This value is not a proper url.",
|
|
22
24
|
"popup.item.form.menuAttached.label": "Attach to menu",
|
|
23
|
-
"popup.item.form.type.label": "
|
|
25
|
+
"popup.item.form.type.label": "Navigation item type",
|
|
24
26
|
"popup.item.form.type.internal.label": "Internal source",
|
|
25
27
|
"popup.item.form.type.external.label": "External source",
|
|
28
|
+
"popup.item.form.type.wrapper.label": "Wrapper element",
|
|
26
29
|
"popup.item.form.type.external.description": "Output path: {value}",
|
|
27
30
|
"popup.item.form.audience.label": "Audience",
|
|
28
31
|
"popup.item.form.audience.placeholder": "Select audience...",
|
|
32
|
+
"popup.item.form.audience.empty": "There are no more audiences",
|
|
29
33
|
"popup.item.form.relatedSection.label": "Relation to",
|
|
30
34
|
"popup.item.form.relatedType.label": "Content Type",
|
|
31
35
|
"popup.item.form.relatedType.placeholder": "Select content type...",
|
|
@@ -81,12 +85,17 @@
|
|
|
81
85
|
"pages.settings.form.nameField.label": "Name fields",
|
|
82
86
|
"pages.settings.form.nameField.placeholder": "Select at least one or leave empty to apply defaults",
|
|
83
87
|
"pages.settings.form.nameField.hint": "If left empty name field is going to take following ordered fields: \"title\", \"subject\" and \"name\"",
|
|
88
|
+
"pages.settings.form.nameField.empty": "This content type doesn't have any string attributes",
|
|
89
|
+
"pages.settings.form.populate.label": "Fields to populate",
|
|
90
|
+
"pages.settings.form.populate.placeholder": "Select at least one or leave empty to disable populating relation fields",
|
|
91
|
+
"pages.settings.form.populate.hint": "Selected relation fields will be populated inside API responses",
|
|
92
|
+
"pages.settings.form.populate.empty": "This content type doesn't have any relation fields",
|
|
84
93
|
"pages.settings.form.contentTypesSettings.label": "Content types",
|
|
85
94
|
"pages.settings.form.contentTypesSettings.tooltip": "Custom configuration per content type",
|
|
86
95
|
"components.navigationItem.action.newItem": "Add nested item",
|
|
87
96
|
"components.navigationItem.badge.removed": "Removed",
|
|
88
|
-
"components.navigationItem.badge.draft": "
|
|
89
|
-
"components.navigationItem.badge.published": "
|
|
97
|
+
"components.navigationItem.badge.draft": "Draft",
|
|
98
|
+
"components.navigationItem.badge.published": "Published",
|
|
90
99
|
"components.confirmation.dialog.button.cancel": "Cancel",
|
|
91
100
|
"components.confirmation.dialog.button.confirm": "Confirm",
|
|
92
101
|
"components.confirmation.dialog.description": "Do you want to continue?",
|
package/package.json
CHANGED
package/server/bootstrap.js
CHANGED
|
@@ -42,30 +42,11 @@ module.exports = async ({ strapi }) => {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
// Initialize configuration
|
|
45
|
-
const
|
|
46
|
-
environment: '',
|
|
47
|
-
type: 'plugin',
|
|
48
|
-
name: 'navigation',
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
const config = await pluginStore.get({ key: 'config' });
|
|
52
|
-
const pluginDefaultConfig = await strapi.plugin('navigation').config
|
|
53
|
-
const defaultConfigValue = {
|
|
54
|
-
additionalFields: pluginDefaultConfig('additionalFields'),
|
|
55
|
-
contentTypes: pluginDefaultConfig('contentTypes'),
|
|
56
|
-
contentTypesNameFields: pluginDefaultConfig('contentTypesNameFields'),
|
|
57
|
-
allowedLevels: pluginDefaultConfig('allowedLevels'),
|
|
58
|
-
gql: pluginDefaultConfig('gql'),
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (!config) {
|
|
62
|
-
pluginStore.set({
|
|
63
|
-
key: 'config', value: defaultConfigValue
|
|
64
|
-
});
|
|
65
|
-
}
|
|
45
|
+
const config = await strapi.plugin('navigation').service('navigation').setDefaultConfig()
|
|
66
46
|
|
|
47
|
+
// Initialize graphql configuration
|
|
67
48
|
if (strapi.plugin('graphql')) {
|
|
68
49
|
const graphqlConfiguration = require('./graphql')
|
|
69
|
-
await graphqlConfiguration({ strapi, config
|
|
50
|
+
await graphqlConfiguration({ strapi, config });
|
|
70
51
|
}
|
|
71
52
|
};
|
package/server/config/index.js
CHANGED
package/server/config.js
CHANGED
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"type": "enumeration",
|
|
38
38
|
"enum": [
|
|
39
39
|
"INTERNAL",
|
|
40
|
-
"EXTERNAL"
|
|
40
|
+
"EXTERNAL",
|
|
41
|
+
"WRAPPER"
|
|
41
42
|
],
|
|
42
43
|
"default": "INTERNAL",
|
|
43
44
|
"configurable": false
|
|
@@ -65,6 +66,11 @@
|
|
|
65
66
|
"default": 0,
|
|
66
67
|
"configurable": false
|
|
67
68
|
},
|
|
69
|
+
"collapsed": {
|
|
70
|
+
"type": "boolean",
|
|
71
|
+
"default": false,
|
|
72
|
+
"configurable": false
|
|
73
|
+
},
|
|
68
74
|
"related": {
|
|
69
75
|
"type": "relation",
|
|
70
76
|
"relation": "oneToOne",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
module.exports = ({ nexus }) => nexus.objectType({
|
|
1
|
+
module.exports = ({ nexus, strapi }) => nexus.objectType({
|
|
2
2
|
name: "ContentTypesNameFields",
|
|
3
3
|
async definition(t) {
|
|
4
4
|
t.nonNull.list.nonNull.string("default")
|
|
5
|
-
const pluginStore = strapi.
|
|
5
|
+
const pluginStore = await strapi.plugin('navigation').service('navigation').getPluginStore();
|
|
6
6
|
const config = await pluginStore.get({ key: 'config' });
|
|
7
7
|
const contentTypesNameFields = config.contentTypesNameFields;
|
|
8
8
|
Object.keys(contentTypesNameFields || {}).forEach(key => t.nonNull.list.string(key))
|
|
@@ -72,10 +72,11 @@ module.exports = ({ strapi }) => {
|
|
|
72
72
|
// Get plugin config
|
|
73
73
|
async config(viaSettingsPage = false) {
|
|
74
74
|
const { audienceModel, service } = utilsFunctions.extractMeta(strapi.plugins);
|
|
75
|
-
const pluginStore = await strapi.
|
|
75
|
+
const pluginStore = await strapi.plugin('navigation').service('navigation').getPluginStore()
|
|
76
76
|
const config = await pluginStore.get({ key: 'config' });
|
|
77
77
|
const additionalFields = config.additionalFields;
|
|
78
78
|
const contentTypesNameFields = config.contentTypesNameFields;
|
|
79
|
+
const contentTypesPopulate = config.contentTypesPopulate;
|
|
79
80
|
const allowedLevels = config.allowedLevels;
|
|
80
81
|
const isGQLPluginEnabled = !isNil(strapi.plugin('graphql'));
|
|
81
82
|
|
|
@@ -86,6 +87,9 @@ module.exports = ({ strapi }) => {
|
|
|
86
87
|
default: contentTypesNameFieldsDefaults,
|
|
87
88
|
...(isObject(contentTypesNameFields) ? contentTypesNameFields : {}),
|
|
88
89
|
},
|
|
90
|
+
contentTypesPopulate: {
|
|
91
|
+
...(isObject(contentTypesPopulate) ? contentTypesPopulate : {}),
|
|
92
|
+
},
|
|
89
93
|
allowedLevels,
|
|
90
94
|
additionalFields,
|
|
91
95
|
isGQLPluginEnabled: viaSettingsPage ? isGQLPluginEnabled : undefined,
|
|
@@ -111,28 +115,42 @@ module.exports = ({ strapi }) => {
|
|
|
111
115
|
},
|
|
112
116
|
|
|
113
117
|
async updateConfig(newConfig) {
|
|
114
|
-
const pluginStore = await strapi.
|
|
118
|
+
const pluginStore = await strapi.plugin('navigation').service('navigation').getPluginStore()
|
|
115
119
|
await pluginStore.set({ key: 'config', value: newConfig });
|
|
116
120
|
},
|
|
117
121
|
|
|
122
|
+
async getPluginStore() {
|
|
123
|
+
return await strapi.store({ type: 'plugin', name: 'navigation' });
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
async setDefaultConfig() {
|
|
127
|
+
const pluginStore = await strapi.plugin('navigation').service('navigation').getPluginStore()
|
|
128
|
+
const config = await pluginStore.get({ key: 'config' });
|
|
129
|
+
const pluginDefaultConfig = await strapi.plugin('navigation').config
|
|
130
|
+
|
|
131
|
+
// If new value gets introduced to the config it either is read from plugin store or from default plugin config
|
|
132
|
+
// This is fix for backwards compatibility and migration of config to newer version of the plugin
|
|
133
|
+
const defaultConfigValue = {
|
|
134
|
+
additionalFields: get(config, 'additionalFields', pluginDefaultConfig('additionalFields')),
|
|
135
|
+
contentTypes: get(config, 'contentTypes', pluginDefaultConfig('contentTypes')),
|
|
136
|
+
contentTypesNameFields: get(config, 'contentTypesNameFields', pluginDefaultConfig('contentTypesNameFields')),
|
|
137
|
+
contentTypesPopulate: get(config, 'contentTypesPopulate', pluginDefaultConfig('contentTypesPopulate')),
|
|
138
|
+
allowedLevels: get(config, 'allowedLevels', pluginDefaultConfig('allowedLevels')),
|
|
139
|
+
gql: get(config, 'gql', pluginDefaultConfig('gql')),
|
|
140
|
+
}
|
|
141
|
+
pluginStore.set({ key: 'config', value: defaultConfigValue });
|
|
142
|
+
|
|
143
|
+
return defaultConfigValue;
|
|
144
|
+
},
|
|
145
|
+
|
|
118
146
|
async restoreConfig() {
|
|
119
|
-
const pluginStore = await strapi.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
await pluginStore.delete({ key: 'config' })
|
|
123
|
-
await pluginStore.set({
|
|
124
|
-
key: 'config', value: {
|
|
125
|
-
additionalFields: defaultConfig('additionalFields'),
|
|
126
|
-
contentTypes: defaultConfig('contentTypes'),
|
|
127
|
-
contentTypesNameFields: defaultConfig('contentTypesNameFields'),
|
|
128
|
-
allowedLevels: defaultConfig('allowedLevels'),
|
|
129
|
-
gql: defaultConfig('gql'),
|
|
130
|
-
}
|
|
131
|
-
});
|
|
147
|
+
const pluginStore = await strapi.plugin('navigation').service('navigation').getPluginStore()
|
|
148
|
+
await pluginStore.delete({ key: 'config' });
|
|
149
|
+
await strapi.plugin('navigation').service('navigation').setDefaultConfig();
|
|
132
150
|
},
|
|
133
151
|
|
|
134
152
|
async configContentTypes() {
|
|
135
|
-
const pluginStore = strapi.
|
|
153
|
+
const pluginStore = await strapi.plugin('navigation').service('navigation').getPluginStore()
|
|
136
154
|
const config = await pluginStore.get({ key: 'config' });
|
|
137
155
|
const eligibleContentTypes =
|
|
138
156
|
await Promise.all(
|
|
@@ -212,6 +230,8 @@ module.exports = ({ strapi }) => {
|
|
|
212
230
|
},
|
|
213
231
|
|
|
214
232
|
async getRelatedItems(entityItems) {
|
|
233
|
+
const pluginStore = await strapi.plugin('navigation').service('navigation').getPluginStore()
|
|
234
|
+
const config = await pluginStore.get({ key: 'config' });
|
|
215
235
|
const relatedTypes = new Set(entityItems.flatMap((item) => get(item.related, 'related_type')));
|
|
216
236
|
const groupedItems = Array.from(relatedTypes).filter((relatedType) => relatedType).reduce(
|
|
217
237
|
(acc, relatedType) => Object.assign(acc, {
|
|
@@ -233,8 +253,9 @@ module.exports = ({ strapi }) => {
|
|
|
233
253
|
.query(model)
|
|
234
254
|
.findMany({
|
|
235
255
|
where: {
|
|
236
|
-
id: { $in: map(related, 'related_id') }
|
|
237
|
-
}
|
|
256
|
+
id: { $in: map(related, 'related_id') },
|
|
257
|
+
},
|
|
258
|
+
populate: config.contentTypesPopulate[model] || []
|
|
238
259
|
});
|
|
239
260
|
return relationData
|
|
240
261
|
.flatMap(_ =>
|
|
@@ -264,8 +285,12 @@ module.exports = ({ strapi }) => {
|
|
|
264
285
|
},
|
|
265
286
|
|
|
266
287
|
async getContentTypeItems(model) {
|
|
288
|
+
const pluginStore = await strapi.plugin('navigation').service('navigation').getPluginStore()
|
|
289
|
+
const config = await pluginStore.get({ key: 'config' });
|
|
267
290
|
try {
|
|
268
|
-
const contentTypeItems = await strapi.query(model).findMany(
|
|
291
|
+
const contentTypeItems = await strapi.query(model).findMany({
|
|
292
|
+
populate: config.contentTypesPopulate[model] || []
|
|
293
|
+
})
|
|
269
294
|
return contentTypeItems;
|
|
270
295
|
} catch (err) {
|
|
271
296
|
return [];
|
|
@@ -213,7 +213,7 @@ module.exports = ({ strapi }) => {
|
|
|
213
213
|
false;
|
|
214
214
|
return item.type === itemType.INTERNAL ? isRelatedDefinedAndPublished : true;
|
|
215
215
|
}
|
|
216
|
-
return (item.type
|
|
216
|
+
return (item.type !== itemType.INTERNAL) || relatedItem;
|
|
217
217
|
},
|
|
218
218
|
};
|
|
219
219
|
}
|