strapi-plugin-navigation 2.0.6 → 2.0.9
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/ConfirmationDialog/index.js +1 -1
- 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 +28 -6
- package/admin/src/components/Item/Wrapper.js +1 -1
- package/admin/src/components/Item/index.js +61 -45
- package/admin/src/components/NavigationItemList/Wrapper.js +1 -1
- package/admin/src/components/NavigationItemList/index.js +3 -0
- package/admin/src/pages/DataManagerProvider/index.js +6 -6
- package/admin/src/pages/SettingsPage/index.js +40 -20
- package/admin/src/pages/View/components/NavigationHeader/index.js +1 -1
- package/admin/src/pages/View/components/NavigationItemForm/index.js +52 -33
- 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 +12 -8
- 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 +47 -27
- package/server/services/utils/functions.js +3 -2
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;
|
|
@@ -27,7 +27,7 @@ const ConfirmationDialog = ({
|
|
|
27
27
|
}) => (
|
|
28
28
|
<Dialog onClose={onCancel} title={header || getMessage('components.confirmation.dialog.header', 'Confirmation')} isOpen={isVisible}>
|
|
29
29
|
<DialogBody icon={<ExclamationMarkCircle />}>
|
|
30
|
-
<Stack
|
|
30
|
+
<Stack spacing={2}>
|
|
31
31
|
<Flex justifyContent="center">
|
|
32
32
|
<Typography id="dialog-confirm-description">{children || getMessage('components.confirmation.dialog.description')}</Typography>
|
|
33
33
|
</Flex>
|
|
@@ -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
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import styled from 'styled-components';
|
|
2
3
|
|
|
3
4
|
import { Flex } from '@strapi/design-system/Flex';
|
|
4
5
|
import { IconButton } from '@strapi/design-system/IconButton';
|
|
@@ -10,20 +11,44 @@ import Wrapper from './Wrapper';
|
|
|
10
11
|
import ItemCardBadge from '../ItemCardBadge';
|
|
11
12
|
import { getMessage } from '../../../utils';
|
|
12
13
|
|
|
14
|
+
const IconWrapper = styled.div`
|
|
15
|
+
display: flex;
|
|
16
|
+
align-items: center;
|
|
17
|
+
justify-content: center;
|
|
18
|
+
height: ${32 / 16}rem;
|
|
19
|
+
width: ${32 / 16}rem;
|
|
20
|
+
|
|
21
|
+
cursor: pointer;
|
|
22
|
+
padding: ${({ theme }) => theme.spaces[2]};
|
|
23
|
+
border-radius: ${({ theme }) => theme.borderRadius};
|
|
24
|
+
background: ${({ theme }) => theme.colors.neutral0};
|
|
25
|
+
border: 1px solid ${({ theme }) => theme.colors.neutral200};
|
|
26
|
+
|
|
27
|
+
svg {
|
|
28
|
+
> g,
|
|
29
|
+
path {
|
|
30
|
+
fill: ${({ theme }) => theme.colors.neutral500};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
`
|
|
34
|
+
|
|
13
35
|
const ItemCardHeader = ({ title, path, icon, removed, onItemRemove, onItemEdit, onItemRestore, dragRef }) => {
|
|
14
36
|
return (
|
|
15
37
|
<Wrapper>
|
|
16
38
|
<Flex alignItems="center">
|
|
17
|
-
<
|
|
39
|
+
<IconWrapper ref={dragRef}>
|
|
40
|
+
<Icon as={Drag} />
|
|
41
|
+
</IconWrapper>
|
|
18
42
|
<Typography variant="omega" fontWeight="bold">
|
|
19
43
|
{title}
|
|
20
44
|
</Typography>
|
|
21
45
|
<Typography variant="omega" fontWeight="bold" textColor='neutral500'>
|
|
22
46
|
{path}
|
|
23
47
|
</Typography>
|
|
48
|
+
<Icon as={icon} />
|
|
24
49
|
</Flex>
|
|
25
50
|
<Flex alignItems="center" style={{ zIndex: 2 }}>
|
|
26
|
-
{removed &&
|
|
51
|
+
{removed &&
|
|
27
52
|
(<ItemCardBadge
|
|
28
53
|
borderColor={`danger200`}
|
|
29
54
|
backgroundColor={`danger100`}
|
|
@@ -36,10 +61,7 @@ const ItemCardHeader = ({ title, path, icon, removed, onItemRemove, onItemEdit,
|
|
|
36
61
|
<IconButton disabled={removed} onClick={onItemEdit} label="Edit" icon={<Pencil />} />
|
|
37
62
|
{removed ?
|
|
38
63
|
<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
|
-
</>
|
|
64
|
+
<IconButton onClick={onItemRemove} label="Remove" icon={<Trash />} />
|
|
43
65
|
}
|
|
44
66
|
</Flex>
|
|
45
67
|
</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 {
|
|
@@ -1,17 +1,15 @@
|
|
|
1
|
-
import React, { useRef
|
|
1
|
+
import React, { useRef } from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
3
|
import { useDrag, useDrop } from 'react-dnd';
|
|
4
|
-
import {
|
|
5
|
-
import { drop, isEmpty, isNumber } from 'lodash';
|
|
4
|
+
import { isEmpty, isNumber } from 'lodash';
|
|
6
5
|
|
|
7
|
-
import { Box } from '@strapi/design-system/Box';
|
|
8
6
|
import { Card, CardBody } from '@strapi/design-system/Card';
|
|
9
7
|
import { Divider } from '@strapi/design-system/Divider';
|
|
10
8
|
import { Flex } from '@strapi/design-system/Flex';
|
|
11
9
|
import { Link } from '@strapi/design-system/Link';
|
|
12
10
|
import { TextButton } from '@strapi/design-system/TextButton';
|
|
13
11
|
import { Typography } from '@strapi/design-system/Typography';
|
|
14
|
-
import { ArrowRight, Link as LinkIcon, Earth, Plus } from '@strapi/icons';
|
|
12
|
+
import { ArrowRight, Link as LinkIcon, Earth, Plus, Cog } from '@strapi/icons';
|
|
15
13
|
|
|
16
14
|
import { navigationItemType } from '../../pages/View/utils/enums';
|
|
17
15
|
import ItemCardHeader from './ItemCardHeader';
|
|
@@ -21,6 +19,7 @@ import { extractRelatedItemLabel } from '../../pages/View/utils/parsers';
|
|
|
21
19
|
import ItemCardBadge from './ItemCardBadge';
|
|
22
20
|
import { ItemCardRemovedOverlay } from './ItemCardRemovedOverlay';
|
|
23
21
|
import { getMessage, ItemTypes } from '../../utils';
|
|
22
|
+
import CollapseButton from '../CollapseButton';
|
|
24
23
|
|
|
25
24
|
const Item = (props) => {
|
|
26
25
|
const {
|
|
@@ -36,6 +35,7 @@ const Item = (props) => {
|
|
|
36
35
|
onItemRestore,
|
|
37
36
|
onItemEdit,
|
|
38
37
|
onItemReOrder,
|
|
38
|
+
onItemToggleCollapse,
|
|
39
39
|
error,
|
|
40
40
|
displayChildren,
|
|
41
41
|
config = {},
|
|
@@ -49,11 +49,14 @@ const Item = (props) => {
|
|
|
49
49
|
removed,
|
|
50
50
|
externalPath,
|
|
51
51
|
menuAttached,
|
|
52
|
+
collapsed,
|
|
52
53
|
} = item;
|
|
53
54
|
|
|
54
55
|
const { contentTypes, contentTypesNameFields } = config;
|
|
55
56
|
const isExternal = type === navigationItemType.EXTERNAL;
|
|
56
|
-
const
|
|
57
|
+
const isWrapper = type === navigationItemType.WRAPPER;
|
|
58
|
+
const isHandledByPublishFlow = relatedRef && typeof relatedRef.publishedAt !== 'undefined';
|
|
59
|
+
const isPublished = isHandledByPublishFlow && relatedRef.publishedAt;
|
|
57
60
|
const isNextMenuAllowedLevel = isNumber(allowedLevels) ? level < (allowedLevels - 1) : true;
|
|
58
61
|
const isMenuAllowedLevel = isNumber(allowedLevels) ? level < allowedLevels : true;
|
|
59
62
|
const hasChildren = !isEmpty(item.items) && !isExternal && !displayChildren;
|
|
@@ -87,7 +90,16 @@ const Item = (props) => {
|
|
|
87
90
|
const isAfter = hoverClientY > hoverMiddleY;
|
|
88
91
|
const newOrder = isAfter ? item.order + 0.5 : item.order - 0.5;
|
|
89
92
|
|
|
93
|
+
if (dragIndex < dropIndex && hoverClientY < hoverMiddleY) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
// Dragging upwards
|
|
97
|
+
if (dragIndex > dropIndex && hoverClientY > hoverMiddleY) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
90
101
|
onItemReOrder({ ...hoveringItem }, newOrder);
|
|
102
|
+
hoveringItem.order = newOrder;
|
|
91
103
|
},
|
|
92
104
|
collect: monitor => ({
|
|
93
105
|
isOverCurrent: monitor.isOver({ shallow: true }),
|
|
@@ -97,7 +109,7 @@ const Item = (props) => {
|
|
|
97
109
|
const [{ isDragging }, drag, dragPreview] = useDrag({
|
|
98
110
|
type: `${ItemTypes.NAVIGATION_ITEM}_${levelPath}`,
|
|
99
111
|
item: () => {
|
|
100
|
-
return { ...item };
|
|
112
|
+
return { ...item, relatedRef };
|
|
101
113
|
},
|
|
102
114
|
collect: monitor => ({
|
|
103
115
|
isDragging: monitor.isDragging(),
|
|
@@ -119,7 +131,7 @@ const Item = (props) => {
|
|
|
119
131
|
<ItemCardHeader
|
|
120
132
|
title={title}
|
|
121
133
|
path={isExternal ? externalPath : absolutePath}
|
|
122
|
-
icon={isExternal ? Earth : LinkIcon}
|
|
134
|
+
icon={isExternal ? Earth : isWrapper ? Cog : LinkIcon}
|
|
123
135
|
onItemRemove={() => onItemRemove({
|
|
124
136
|
...item,
|
|
125
137
|
relatedRef,
|
|
@@ -128,6 +140,7 @@ const Item = (props) => {
|
|
|
128
140
|
...item,
|
|
129
141
|
isMenuAllowedLevel,
|
|
130
142
|
isParentAttachedToMenu,
|
|
143
|
+
relatedRef,
|
|
131
144
|
}, levelPath, isParentAttachedToMenu)}
|
|
132
145
|
onItemRestore={() => onItemRestore({
|
|
133
146
|
...item,
|
|
@@ -138,49 +151,50 @@ const Item = (props) => {
|
|
|
138
151
|
/>
|
|
139
152
|
</CardBody>
|
|
140
153
|
<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
|
-
|
|
154
|
+
{!isExternal && (
|
|
155
|
+
<CardBody style={{ padding: '8px' }}>
|
|
156
|
+
<Flex style={{ width: '100%' }} direction="row" alignItems="center" justifyContent="space-between">
|
|
157
|
+
<Flex>
|
|
158
|
+
{!isEmpty(item.items) && <CollapseButton toggle={() => onItemToggleCollapse({...item, relatedRef})} collapsed={collapsed} itemsCount={item.items.length}/>}
|
|
159
|
+
<TextButton
|
|
160
|
+
disabled={removed}
|
|
161
|
+
startIcon={<Plus />}
|
|
162
|
+
onClick={(e) => onItemLevelAdd(e, viewId, isNextMenuAllowedLevel, absolutePath, menuAttached)}
|
|
163
|
+
>
|
|
164
|
+
<Typography variant="pi" fontWeight="bold" textColor={removed ? "neutral600" : "primary600"}>
|
|
165
|
+
{getMessage("components.navigationItem.action.newItem")}
|
|
166
|
+
</Typography>
|
|
167
|
+
</TextButton>
|
|
168
|
+
</Flex>
|
|
169
|
+
{relatedItemLabel && (
|
|
170
|
+
<Flex justifyContent='center' alignItems='center'>
|
|
171
|
+
{isHandledByPublishFlow && <ItemCardBadge
|
|
172
|
+
borderColor={`${relatedBadgeColor}200`}
|
|
173
|
+
backgroundColor={`${relatedBadgeColor}100`}
|
|
174
|
+
textColor={`${relatedBadgeColor}600`}
|
|
175
|
+
className="action"
|
|
176
|
+
small
|
|
177
|
+
>
|
|
178
|
+
{getMessage({ id: `components.navigationItem.badge.${isPublished ? 'published' : 'draft'}` })}
|
|
179
|
+
</ItemCardBadge>}
|
|
180
|
+
<Typography variant="omega" textColor='neutral600'>{relatedTypeLabel} / </Typography>
|
|
181
|
+
<Typography variant="omega" textColor='neutral800'>{relatedItemLabel}</Typography>
|
|
182
|
+
<Link
|
|
183
|
+
to={`/content-manager/collectionType/${relatedRef?.__collectionUid}/${relatedRef?.id}`}
|
|
184
|
+
endIcon={<ArrowRight />}> </Link>
|
|
185
|
+
</Flex>)
|
|
186
|
+
}
|
|
187
|
+
</Flex>
|
|
188
|
+
</CardBody>)}
|
|
176
189
|
</div>
|
|
177
190
|
</Card>
|
|
178
|
-
{hasChildren && !removed && <List
|
|
191
|
+
{hasChildren && !removed && !collapsed && <List
|
|
179
192
|
onItemLevelAdd={onItemLevelAdd}
|
|
180
193
|
onItemRemove={onItemRemove}
|
|
181
194
|
onItemEdit={onItemEdit}
|
|
182
195
|
onItemRestore={onItemRestore}
|
|
183
196
|
onItemReOrder={onItemReOrder}
|
|
197
|
+
onItemToggleCollapse={onItemToggleCollapse}
|
|
184
198
|
error={error}
|
|
185
199
|
allowedLevels={allowedLevels}
|
|
186
200
|
isParentAttachedToMenu={menuAttached}
|
|
@@ -204,7 +218,8 @@ Item.propTypes = {
|
|
|
204
218
|
path: PropTypes.string,
|
|
205
219
|
externalPath: PropTypes.string,
|
|
206
220
|
related: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
207
|
-
menuAttached: PropTypes.bool
|
|
221
|
+
menuAttached: PropTypes.bool,
|
|
222
|
+
collapsed: PropTypes.bool,
|
|
208
223
|
}).isRequired,
|
|
209
224
|
relatedRef: PropTypes.object,
|
|
210
225
|
level: PropTypes.number,
|
|
@@ -214,6 +229,7 @@ Item.propTypes = {
|
|
|
214
229
|
onItemLevelAdd: PropTypes.func.isRequired,
|
|
215
230
|
onItemRemove: PropTypes.func.isRequired,
|
|
216
231
|
onItemReOrder: PropTypes.func.isRequired,
|
|
232
|
+
onItemToggleCollapse: PropTypes.func.isRequired,
|
|
217
233
|
config: PropTypes.shape({
|
|
218
234
|
contentTypes: PropTypes.array.isRequired,
|
|
219
235
|
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
|
};
|
|
@@ -94,8 +94,8 @@ const DataManagerProvider = ({ children }) => {
|
|
|
94
94
|
} catch (err) {
|
|
95
95
|
console.error({ err });
|
|
96
96
|
toggleNotification({
|
|
97
|
-
type: '
|
|
98
|
-
message: { id: 'notification.error' },
|
|
97
|
+
type: 'warning',
|
|
98
|
+
message: { id: getTrad('notification.error') },
|
|
99
99
|
});
|
|
100
100
|
}
|
|
101
101
|
};
|
|
@@ -133,8 +133,8 @@ const DataManagerProvider = ({ children }) => {
|
|
|
133
133
|
} catch (err) {
|
|
134
134
|
console.error({ err });
|
|
135
135
|
toggleNotification({
|
|
136
|
-
type: '
|
|
137
|
-
message: { id: 'notification.error' },
|
|
136
|
+
type: 'warning',
|
|
137
|
+
message: { id: getTrad('notification.error') },
|
|
138
138
|
});
|
|
139
139
|
}
|
|
140
140
|
};
|
|
@@ -254,7 +254,7 @@ const DataManagerProvider = ({ children }) => {
|
|
|
254
254
|
|
|
255
255
|
if (err.response.payload.data && err.response.payload.data.errorTitles) {
|
|
256
256
|
return toggleNotification({
|
|
257
|
-
type: '
|
|
257
|
+
type: 'warning',
|
|
258
258
|
message: {
|
|
259
259
|
id: formatMessage(
|
|
260
260
|
getTrad('notification.navigation.error'),
|
|
@@ -264,7 +264,7 @@ const DataManagerProvider = ({ children }) => {
|
|
|
264
264
|
});
|
|
265
265
|
}
|
|
266
266
|
toggleNotification({
|
|
267
|
-
type: '
|
|
267
|
+
type: 'warning',
|
|
268
268
|
message: { id: getTrad('notification.error') },
|
|
269
269
|
});
|
|
270
270
|
}
|
|
@@ -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
|
>
|
|
@@ -142,7 +145,7 @@ const SettingsPage = () => {
|
|
|
142
145
|
}
|
|
143
146
|
/>
|
|
144
147
|
<ContentLayout>
|
|
145
|
-
<Stack
|
|
148
|
+
<Stack spacing={7}>
|
|
146
149
|
{isRestartRequired && (
|
|
147
150
|
<RestartAlert
|
|
148
151
|
closeLabel={getMessage('pages.settings.actions.restart.alert.cancel')}
|
|
@@ -152,7 +155,7 @@ const SettingsPage = () => {
|
|
|
152
155
|
{getMessage('pages.settings.actions.restart.alert.description')}
|
|
153
156
|
</RestartAlert>)}
|
|
154
157
|
<Box {...boxDefaultProps} >
|
|
155
|
-
<Stack
|
|
158
|
+
<Stack spacing={4}>
|
|
156
159
|
<Typography variant="delta" as="h2">
|
|
157
160
|
{getMessage('pages.settings.general.title')}
|
|
158
161
|
</Typography>
|
|
@@ -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}
|
|
@@ -193,24 +197,40 @@ const SettingsPage = () => {
|
|
|
193
197
|
<AccordionToggle title={displayName} togglePosition="left" />
|
|
194
198
|
<AccordionContent>
|
|
195
199
|
<Box padding={6}>
|
|
196
|
-
<Stack
|
|
200
|
+
<Stack spacing={4}>
|
|
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
|
})}
|
|
@@ -220,22 +240,22 @@ const SettingsPage = () => {
|
|
|
220
240
|
</Stack>
|
|
221
241
|
</Box>
|
|
222
242
|
<Box {...boxDefaultProps} >
|
|
223
|
-
<Stack
|
|
243
|
+
<Stack spacing={4}>
|
|
224
244
|
<Typography variant="delta" as="h2">
|
|
225
245
|
{getMessage('pages.settings.additional.title')}
|
|
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"
|
|
@@ -252,7 +272,7 @@ const SettingsPage = () => {
|
|
|
252
272
|
</Stack>
|
|
253
273
|
</Box>
|
|
254
274
|
<Box {...boxDefaultProps} >
|
|
255
|
-
<Stack
|
|
275
|
+
<Stack spacing={4}>
|
|
256
276
|
<Typography variant="delta" as="h2">
|
|
257
277
|
{getMessage('pages.settings.restoring.title')}
|
|
258
278
|
</Typography>
|
|
@@ -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,22 +74,23 @@ 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
|
-
const title =
|
|
80
|
+
const title = isSingleSelected ?
|
|
81
|
+
relatedTypeSelectOptions.find(v => v.key == relatedType).label :
|
|
82
|
+
payload.title || relatedSelectOptions.find(v => v.key == relatedId)?.label;
|
|
84
83
|
return {
|
|
85
84
|
...purePayload,
|
|
86
85
|
title,
|
|
86
|
+
type,
|
|
87
87
|
menuAttached: isNil(menuAttached) ? false : menuAttached,
|
|
88
|
-
type:
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
relatedType: relatedCollectionType,
|
|
88
|
+
path: type !== navigationItemType.EXTERNAL ? purePayload.path : undefined,
|
|
89
|
+
externalPath: type === navigationItemType.EXTERNAL ? purePayload.externalPath : undefined,
|
|
90
|
+
related: type === navigationItemType.INTERNAL ? relatedId : undefined,
|
|
91
|
+
relatedType: type === navigationItemType.INTERNAL ? relatedCollectionType : undefined,
|
|
93
92
|
isSingle: isSingleSelected,
|
|
94
|
-
uiRouterKey: generateUiRouterKey(
|
|
93
|
+
uiRouterKey: generateUiRouterKey(title, relatedId, relatedCollectionType),
|
|
95
94
|
};
|
|
96
95
|
};
|
|
97
96
|
|
|
@@ -109,11 +108,8 @@ const NavigationItemForm = ({
|
|
|
109
108
|
}
|
|
110
109
|
};
|
|
111
110
|
|
|
112
|
-
const onTypeChange = ({ target: { name, value } }) =>
|
|
113
|
-
onChange({ target: { name, value: value ? navigationItemType.INTERNAL : navigationItemType.EXTERNAL } });
|
|
114
|
-
|
|
115
111
|
const onAudienceChange = (value) => {
|
|
116
|
-
onChange({target: {name: `${inputsPrefix}audience`, value}});
|
|
112
|
+
onChange({ target: { name: `${inputsPrefix}audience`, value } });
|
|
117
113
|
}
|
|
118
114
|
|
|
119
115
|
const onChange = ({ target: { name, value } }) => {
|
|
@@ -148,6 +144,20 @@ const NavigationItemForm = ({
|
|
|
148
144
|
[relatedTypeSelectValue, contentTypes],
|
|
149
145
|
);
|
|
150
146
|
|
|
147
|
+
const navigationItemTypeOptions = Object.keys(navigationItemType).map(key => {
|
|
148
|
+
const value = navigationItemType[key].toLowerCase();
|
|
149
|
+
return {
|
|
150
|
+
key,
|
|
151
|
+
value: navigationItemType[key],
|
|
152
|
+
metadatas: {
|
|
153
|
+
intlLabel: {
|
|
154
|
+
id: getTradId(`popup.item.form.type.${value}.label`),
|
|
155
|
+
defaultMessage: getTradId(`popup.item.form.type.${value}.label`),
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
151
161
|
const relatedSelectOptions = contentTypeEntities
|
|
152
162
|
.filter((item) => {
|
|
153
163
|
const usedContentTypeEntitiesOfSameType = usedContentTypeEntities
|
|
@@ -178,7 +188,9 @@ const NavigationItemForm = ({
|
|
|
178
188
|
const isExternal = form.type === navigationItemType.EXTERNAL;
|
|
179
189
|
const pathSourceName = isExternal ? 'externalPath' : 'path';
|
|
180
190
|
|
|
181
|
-
const submitDisabled =
|
|
191
|
+
const submitDisabled =
|
|
192
|
+
(form.type === navigationItemType.INTERNAL && !isSingleSelected && isNil(get(form, `${inputsPrefix}related`))) ||
|
|
193
|
+
(form.type === navigationItemType.WRAPPER && isNil(get(form, `${inputsPrefix}title`)));
|
|
182
194
|
|
|
183
195
|
const debouncedSearch = useCallback(
|
|
184
196
|
debounce(nextValue => setContentTypeSearchQuery(nextValue), 500),
|
|
@@ -222,7 +234,7 @@ const NavigationItemForm = ({
|
|
|
222
234
|
}
|
|
223
235
|
},
|
|
224
236
|
value: get(item, 'uid'),
|
|
225
|
-
label:
|
|
237
|
+
label: get(item, 'label', get(item, 'name')),
|
|
226
238
|
})),
|
|
227
239
|
[contentTypes, usedContentTypesData],
|
|
228
240
|
);
|
|
@@ -285,7 +297,21 @@ const NavigationItemForm = ({
|
|
|
285
297
|
value={get(form, `${inputsPrefix}title`, '')}
|
|
286
298
|
/>
|
|
287
299
|
</GridItem>
|
|
288
|
-
<GridItem key={`${inputsPrefix}
|
|
300
|
+
<GridItem key={`${inputsPrefix}type`} col={4} lg={12}>
|
|
301
|
+
<GenericInput
|
|
302
|
+
intlLabel={{
|
|
303
|
+
id: getTradId('popup.item.form.type.label'),
|
|
304
|
+
defaultMessage: 'Internal link',
|
|
305
|
+
}}
|
|
306
|
+
name={`${inputsPrefix}type`}
|
|
307
|
+
options={navigationItemTypeOptions}
|
|
308
|
+
type='select'
|
|
309
|
+
error={get(formErrors, `${inputsPrefix}type.id`)}
|
|
310
|
+
onChange={onChange}
|
|
311
|
+
value={get(form, `${inputsPrefix}type`, '')}
|
|
312
|
+
/>
|
|
313
|
+
</GridItem>
|
|
314
|
+
<GridItem key={`${inputsPrefix}menuAttached`} col={4} lg={12}>
|
|
289
315
|
<GenericInput
|
|
290
316
|
intlLabel={{
|
|
291
317
|
id: getTradId('popup.item.form.menuAttached.label'),
|
|
@@ -299,19 +325,6 @@ const NavigationItemForm = ({
|
|
|
299
325
|
disabled={!(data.isMenuAllowedLevel && data.parentAttachedToMenu)}
|
|
300
326
|
/>
|
|
301
327
|
</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
328
|
<GridItem key={`${inputsPrefix}path`} col={12}>
|
|
316
329
|
<GenericInput
|
|
317
330
|
intlLabel={{
|
|
@@ -330,7 +343,7 @@ const NavigationItemForm = ({
|
|
|
330
343
|
description={generatePreviewPath()}
|
|
331
344
|
/>
|
|
332
345
|
</GridItem>
|
|
333
|
-
{
|
|
346
|
+
{get(form, `${inputsPrefix}type`) === navigationItemType.INTERNAL && (
|
|
334
347
|
<>
|
|
335
348
|
<GridItem col={6} lg={12}>
|
|
336
349
|
<GenericInput
|
|
@@ -402,8 +415,14 @@ const NavigationItemForm = ({
|
|
|
402
415
|
label={getMessage('popup.item.form.audience.label')}
|
|
403
416
|
onChange={onAudienceChange}
|
|
404
417
|
value={audience}
|
|
418
|
+
hint={
|
|
419
|
+
!isLoading && isEmpty(audienceOptions)
|
|
420
|
+
? getMessage('popup.item.form.audience.empty', 'There are no more audiences')
|
|
421
|
+
: undefined
|
|
422
|
+
}
|
|
405
423
|
multi
|
|
406
424
|
withTags
|
|
425
|
+
disabled={isEmpty(audienceOptions)}
|
|
407
426
|
>
|
|
408
427
|
{audienceOptions.map(({ value, label }) => <Option key={value} value={value}>{label}</Option>)}
|
|
409
428
|
</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
|
{
|
|
@@ -119,7 +122,7 @@ const linkRelations = (item, config) => {
|
|
|
119
122
|
|
|
120
123
|
const shouldFindRelated = (isNumber(related) || isUuid(related) || isString(related)) && !relatedRef;
|
|
121
124
|
const shouldBuildRelated = !relatedRef || (relatedRef && (relatedRef.id !== relatedId));
|
|
122
|
-
|
|
125
|
+
|
|
123
126
|
if (shouldBuildRelated && !shouldFindRelated) {
|
|
124
127
|
const relatedContentType = find(contentTypes,
|
|
125
128
|
ct => ct.uid === relatedItem.__contentType, {});
|
|
@@ -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 }) => {
|
|
@@ -292,9 +295,10 @@ export const isRelationPublished = ({ relatedRef, relatedType = {}, type, isColl
|
|
|
292
295
|
|
|
293
296
|
export const validateNavigationStructure = (items = []) =>
|
|
294
297
|
items.map(item =>
|
|
295
|
-
(
|
|
296
|
-
|
|
297
|
-
type: item.type
|
|
298
|
-
|
|
298
|
+
(
|
|
299
|
+
item.removed ||
|
|
300
|
+
isRelationCorrect({ related: item.related, type: item.type }) ||
|
|
301
|
+
(item.isSingle && isRelationCorrect({ related: item.relatedType, type: item.type }))
|
|
302
|
+
) &&
|
|
299
303
|
validateNavigationStructure(item.items)
|
|
300
304
|
).filter(item => !item).length === 0;
|
|
@@ -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))
|
|
@@ -33,9 +33,7 @@ module.exports = ({ strapi }) => {
|
|
|
33
33
|
const entities = await strapi
|
|
34
34
|
.query(masterModel.uid)
|
|
35
35
|
.findMany({
|
|
36
|
-
|
|
37
|
-
limit: -1,
|
|
38
|
-
}
|
|
36
|
+
limit: -1,
|
|
39
37
|
});
|
|
40
38
|
return entities;
|
|
41
39
|
},
|
|
@@ -52,9 +50,7 @@ module.exports = ({ strapi }) => {
|
|
|
52
50
|
where: {
|
|
53
51
|
master: id,
|
|
54
52
|
},
|
|
55
|
-
|
|
56
|
-
limit: -1,
|
|
57
|
-
},
|
|
53
|
+
limit: -1,
|
|
58
54
|
sort: ['order:asc'],
|
|
59
55
|
populate: ['related', 'parent', 'audience']
|
|
60
56
|
});
|
|
@@ -72,10 +68,11 @@ module.exports = ({ strapi }) => {
|
|
|
72
68
|
// Get plugin config
|
|
73
69
|
async config(viaSettingsPage = false) {
|
|
74
70
|
const { audienceModel, service } = utilsFunctions.extractMeta(strapi.plugins);
|
|
75
|
-
const pluginStore = await strapi.
|
|
71
|
+
const pluginStore = await strapi.plugin('navigation').service('navigation').getPluginStore()
|
|
76
72
|
const config = await pluginStore.get({ key: 'config' });
|
|
77
73
|
const additionalFields = config.additionalFields;
|
|
78
74
|
const contentTypesNameFields = config.contentTypesNameFields;
|
|
75
|
+
const contentTypesPopulate = config.contentTypesPopulate;
|
|
79
76
|
const allowedLevels = config.allowedLevels;
|
|
80
77
|
const isGQLPluginEnabled = !isNil(strapi.plugin('graphql'));
|
|
81
78
|
|
|
@@ -86,6 +83,9 @@ module.exports = ({ strapi }) => {
|
|
|
86
83
|
default: contentTypesNameFieldsDefaults,
|
|
87
84
|
...(isObject(contentTypesNameFields) ? contentTypesNameFields : {}),
|
|
88
85
|
},
|
|
86
|
+
contentTypesPopulate: {
|
|
87
|
+
...(isObject(contentTypesPopulate) ? contentTypesPopulate : {}),
|
|
88
|
+
},
|
|
89
89
|
allowedLevels,
|
|
90
90
|
additionalFields,
|
|
91
91
|
isGQLPluginEnabled: viaSettingsPage ? isGQLPluginEnabled : undefined,
|
|
@@ -111,28 +111,42 @@ module.exports = ({ strapi }) => {
|
|
|
111
111
|
},
|
|
112
112
|
|
|
113
113
|
async updateConfig(newConfig) {
|
|
114
|
-
const pluginStore = await strapi.
|
|
114
|
+
const pluginStore = await strapi.plugin('navigation').service('navigation').getPluginStore()
|
|
115
115
|
await pluginStore.set({ key: 'config', value: newConfig });
|
|
116
116
|
},
|
|
117
117
|
|
|
118
|
+
async getPluginStore() {
|
|
119
|
+
return await strapi.store({ type: 'plugin', name: 'navigation' });
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
async setDefaultConfig() {
|
|
123
|
+
const pluginStore = await strapi.plugin('navigation').service('navigation').getPluginStore()
|
|
124
|
+
const config = await pluginStore.get({ key: 'config' });
|
|
125
|
+
const pluginDefaultConfig = await strapi.plugin('navigation').config
|
|
126
|
+
|
|
127
|
+
// If new value gets introduced to the config it either is read from plugin store or from default plugin config
|
|
128
|
+
// This is fix for backwards compatibility and migration of config to newer version of the plugin
|
|
129
|
+
const defaultConfigValue = {
|
|
130
|
+
additionalFields: get(config, 'additionalFields', pluginDefaultConfig('additionalFields')),
|
|
131
|
+
contentTypes: get(config, 'contentTypes', pluginDefaultConfig('contentTypes')),
|
|
132
|
+
contentTypesNameFields: get(config, 'contentTypesNameFields', pluginDefaultConfig('contentTypesNameFields')),
|
|
133
|
+
contentTypesPopulate: get(config, 'contentTypesPopulate', pluginDefaultConfig('contentTypesPopulate')),
|
|
134
|
+
allowedLevels: get(config, 'allowedLevels', pluginDefaultConfig('allowedLevels')),
|
|
135
|
+
gql: get(config, 'gql', pluginDefaultConfig('gql')),
|
|
136
|
+
}
|
|
137
|
+
pluginStore.set({ key: 'config', value: defaultConfigValue });
|
|
138
|
+
|
|
139
|
+
return defaultConfigValue;
|
|
140
|
+
},
|
|
141
|
+
|
|
118
142
|
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
|
-
});
|
|
143
|
+
const pluginStore = await strapi.plugin('navigation').service('navigation').getPluginStore()
|
|
144
|
+
await pluginStore.delete({ key: 'config' });
|
|
145
|
+
await strapi.plugin('navigation').service('navigation').setDefaultConfig();
|
|
132
146
|
},
|
|
133
147
|
|
|
134
148
|
async configContentTypes() {
|
|
135
|
-
const pluginStore = strapi.
|
|
149
|
+
const pluginStore = await strapi.plugin('navigation').service('navigation').getPluginStore()
|
|
136
150
|
const config = await pluginStore.get({ key: 'config' });
|
|
137
151
|
const eligibleContentTypes =
|
|
138
152
|
await Promise.all(
|
|
@@ -212,6 +226,8 @@ module.exports = ({ strapi }) => {
|
|
|
212
226
|
},
|
|
213
227
|
|
|
214
228
|
async getRelatedItems(entityItems) {
|
|
229
|
+
const pluginStore = await strapi.plugin('navigation').service('navigation').getPluginStore()
|
|
230
|
+
const config = await pluginStore.get({ key: 'config' });
|
|
215
231
|
const relatedTypes = new Set(entityItems.flatMap((item) => get(item.related, 'related_type')));
|
|
216
232
|
const groupedItems = Array.from(relatedTypes).filter((relatedType) => relatedType).reduce(
|
|
217
233
|
(acc, relatedType) => Object.assign(acc, {
|
|
@@ -233,8 +249,9 @@ module.exports = ({ strapi }) => {
|
|
|
233
249
|
.query(model)
|
|
234
250
|
.findMany({
|
|
235
251
|
where: {
|
|
236
|
-
id: { $in: map(related, 'related_id') }
|
|
237
|
-
}
|
|
252
|
+
id: { $in: map(related, 'related_id') },
|
|
253
|
+
},
|
|
254
|
+
populate: config.contentTypesPopulate[model] || []
|
|
238
255
|
});
|
|
239
256
|
return relationData
|
|
240
257
|
.flatMap(_ =>
|
|
@@ -264,8 +281,12 @@ module.exports = ({ strapi }) => {
|
|
|
264
281
|
},
|
|
265
282
|
|
|
266
283
|
async getContentTypeItems(model) {
|
|
284
|
+
const pluginStore = await strapi.plugin('navigation').service('navigation').getPluginStore()
|
|
285
|
+
const config = await pluginStore.get({ key: 'config' });
|
|
267
286
|
try {
|
|
268
|
-
const contentTypeItems = await strapi.query(model).findMany(
|
|
287
|
+
const contentTypeItems = await strapi.query(model).findMany({
|
|
288
|
+
populate: config.contentTypesPopulate[model] || []
|
|
289
|
+
})
|
|
269
290
|
return contentTypeItems;
|
|
270
291
|
} catch (err) {
|
|
271
292
|
return [];
|
|
@@ -607,7 +628,7 @@ module.exports = ({ strapi }) => {
|
|
|
607
628
|
}
|
|
608
629
|
const navigationItem = await strapi
|
|
609
630
|
.query(itemModel.uid)
|
|
610
|
-
.create({ data });
|
|
631
|
+
.create({ data, populate: ['related', 'items'] });
|
|
611
632
|
return !isEmpty(item.items)
|
|
612
633
|
? service.createBranch(
|
|
613
634
|
item.items,
|
|
@@ -710,7 +731,6 @@ module.exports = ({ strapi }) => {
|
|
|
710
731
|
if (relatedItems) {
|
|
711
732
|
return Promise.all(relatedItems.map(async relatedItem => {
|
|
712
733
|
try {
|
|
713
|
-
|
|
714
734
|
const model = strapi.query('plugin::navigation.navigations-items-related');
|
|
715
735
|
const entity = await model
|
|
716
736
|
.findOne({
|
|
@@ -7,7 +7,8 @@ const {
|
|
|
7
7
|
isString,
|
|
8
8
|
get,
|
|
9
9
|
isNil,
|
|
10
|
-
isArray
|
|
10
|
+
isArray,
|
|
11
|
+
first,
|
|
11
12
|
} = require('lodash');
|
|
12
13
|
|
|
13
14
|
const { type: itemType } = require('../../content-types/navigation-item/lifecycle');
|
|
@@ -213,7 +214,7 @@ module.exports = ({ strapi }) => {
|
|
|
213
214
|
false;
|
|
214
215
|
return item.type === itemType.INTERNAL ? isRelatedDefinedAndPublished : true;
|
|
215
216
|
}
|
|
216
|
-
return (item.type
|
|
217
|
+
return (item.type !== itemType.INTERNAL) || relatedItem;
|
|
217
218
|
},
|
|
218
219
|
};
|
|
219
220
|
}
|