strapi-plugin-navigation 2.0.2 β 2.0.5
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 +15 -1
- package/__mocks__/strapi.js +1 -1
- package/admin/src/components/Item/ItemCardHeader/index.js +6 -3
- package/admin/src/components/Item/index.js +108 -53
- package/admin/src/components/NavigationItemList/index.js +3 -0
- package/admin/src/components/Search/index.js +1 -1
- package/admin/src/pages/SettingsPage/index.js +73 -89
- package/admin/src/pages/View/components/NavigationItemForm/index.js +4 -2
- package/admin/src/pages/View/index.js +8 -0
- package/admin/src/translations/en.json +3 -0
- package/admin/src/utils/index.js +4 -2
- package/package.json +2 -2
- package/server/controllers/navigation.js +4 -3
- package/server/graphql/queries/render-navigation.js +4 -3
- package/server/graphql/types/navigation-item.js +1 -1
- package/server/services/__tests__/functions.test.js +48 -0
- package/server/services/__tests__/navigation.test.js +26 -4
- package/server/services/navigation.js +18 -9
- package/server/services/utils/functions.js +41 -0
package/README.md
CHANGED
|
@@ -81,7 +81,7 @@ Complete installation requirements are exact same as for Strapi itself and can b
|
|
|
81
81
|
|
|
82
82
|
**Supported Strapi versions**:
|
|
83
83
|
|
|
84
|
-
- Strapi v4.0
|
|
84
|
+
- Strapi v4.1.0 (recently tested)
|
|
85
85
|
- Strapi v4.x
|
|
86
86
|
|
|
87
87
|
> This plugin is designed for **Strapi v4** and is not working with v3.x. To get version for **Strapi v3** install version [v1.x](https://github.com/VirtusLab-Open-Source/strapi-plugin-navigation/tree/strapi-v3).
|
|
@@ -233,6 +233,20 @@ For any role different than **Super Admin**, to access the **Navigation panel**
|
|
|
233
233
|
|
|
234
234
|
## πΈοΈ Public API specification
|
|
235
235
|
|
|
236
|
+
### Query Params
|
|
237
|
+
|
|
238
|
+
- `type` - Enum value representing structure type of returned navigation
|
|
239
|
+
|
|
240
|
+
**Example URL**: `https://localhost:1337/api/navigation/render/1?type=FLAT`
|
|
241
|
+
|
|
242
|
+
- `menu` - Boolean value for querying only navigation items that are attached to menu should be rendered eg.
|
|
243
|
+
|
|
244
|
+
**Example URL**: `https://localhost:1337/api/navigation/render/1?menu=true`
|
|
245
|
+
|
|
246
|
+
- `path` - String value for querying navigation items by its path
|
|
247
|
+
|
|
248
|
+
**Example URL**: `https://localhost:1337/api/navigation/render/1?path=/home/about-us`
|
|
249
|
+
|
|
236
250
|
### Render
|
|
237
251
|
|
|
238
252
|
`GET <host>/api/navigation/render/<idOrSlug>?type=<type>`
|
package/__mocks__/strapi.js
CHANGED
|
@@ -4,13 +4,13 @@ import { Flex } from '@strapi/design-system/Flex';
|
|
|
4
4
|
import { IconButton } from '@strapi/design-system/IconButton';
|
|
5
5
|
import { Typography } from '@strapi/design-system/Typography';
|
|
6
6
|
import { Icon } from '@strapi/design-system/Icon';
|
|
7
|
-
import { Pencil, Trash, Refresh } from '@strapi/icons/';
|
|
7
|
+
import { Pencil, Trash, Refresh, Drag } from '@strapi/icons/';
|
|
8
8
|
|
|
9
9
|
import Wrapper from './Wrapper';
|
|
10
10
|
import ItemCardBadge from '../ItemCardBadge';
|
|
11
11
|
import { getMessage } from '../../../utils';
|
|
12
12
|
|
|
13
|
-
const ItemCardHeader = ({ title, path, icon, removed, onItemRemove, onItemEdit, onItemRestore }) => {
|
|
13
|
+
const ItemCardHeader = ({ title, path, icon, removed, onItemRemove, onItemEdit, onItemRestore, dragRef }) => {
|
|
14
14
|
return (
|
|
15
15
|
<Wrapper>
|
|
16
16
|
<Flex alignItems="center">
|
|
@@ -36,7 +36,10 @@ const ItemCardHeader = ({ title, path, icon, removed, onItemRemove, onItemEdit,
|
|
|
36
36
|
<IconButton disabled={removed} onClick={onItemEdit} label="Edit" icon={<Pencil />} />
|
|
37
37
|
{removed ?
|
|
38
38
|
<IconButton onClick={onItemRestore} label="Restore" icon={<Refresh />} /> :
|
|
39
|
-
|
|
39
|
+
<>
|
|
40
|
+
<IconButton ref={dragRef} label="Drag" icon={<Drag />} />
|
|
41
|
+
<IconButton onClick={onItemRemove} label="Remove" icon={<Trash />} />
|
|
42
|
+
</>
|
|
40
43
|
}
|
|
41
44
|
</Flex>
|
|
42
45
|
</Wrapper>
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import React, { useRef, useEffect } from 'react';
|
|
1
2
|
import PropTypes from 'prop-types';
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
3
|
+
import { useDrag, useDrop } from 'react-dnd';
|
|
4
|
+
import { getEmptyImage } from 'react-dnd-html5-backend';
|
|
5
|
+
import { drop, isEmpty, isNumber } from 'lodash';
|
|
4
6
|
|
|
5
7
|
import { Box } from '@strapi/design-system/Box';
|
|
6
8
|
import { Card, CardBody } from '@strapi/design-system/Card';
|
|
@@ -18,7 +20,7 @@ import Wrapper from './Wrapper';
|
|
|
18
20
|
import { extractRelatedItemLabel } from '../../pages/View/utils/parsers';
|
|
19
21
|
import ItemCardBadge from './ItemCardBadge';
|
|
20
22
|
import { ItemCardRemovedOverlay } from './ItemCardRemovedOverlay';
|
|
21
|
-
import { getMessage } from '../../utils';
|
|
23
|
+
import { getMessage, ItemTypes } from '../../utils';
|
|
22
24
|
|
|
23
25
|
const Item = (props) => {
|
|
24
26
|
const {
|
|
@@ -33,6 +35,7 @@ const Item = (props) => {
|
|
|
33
35
|
onItemRemove,
|
|
34
36
|
onItemRestore,
|
|
35
37
|
onItemEdit,
|
|
38
|
+
onItemReOrder,
|
|
36
39
|
error,
|
|
37
40
|
displayChildren,
|
|
38
41
|
config = {},
|
|
@@ -60,73 +63,124 @@ const Item = (props) => {
|
|
|
60
63
|
const relatedTypeLabel = relatedRef?.labelSingular;
|
|
61
64
|
const relatedBadgeColor = isPublished ? 'success' : 'secondary';
|
|
62
65
|
|
|
66
|
+
const dragRef = useRef(null);
|
|
67
|
+
const dropRef = useRef(null);
|
|
68
|
+
const previewRef = useRef(null);
|
|
69
|
+
|
|
70
|
+
const [, drop] = useDrop({
|
|
71
|
+
accept: `${ItemTypes.NAVIGATION_ITEM}_${levelPath}`,
|
|
72
|
+
hover(hoveringItem, monitor) {
|
|
73
|
+
const dragIndex = hoveringItem.order;
|
|
74
|
+
const dropIndex = item.order;
|
|
75
|
+
|
|
76
|
+
// Don't replace items with themselves
|
|
77
|
+
if (dragIndex === dropIndex) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const hoverBoundingRect = dropRef.current.getBoundingClientRect();
|
|
82
|
+
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
|
|
83
|
+
const clientOffset = monitor.getClientOffset();
|
|
84
|
+
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
|
|
85
|
+
|
|
86
|
+
// Place the hovering item before or after the drop target
|
|
87
|
+
const isAfter = hoverClientY > hoverMiddleY;
|
|
88
|
+
const newOrder = isAfter ? item.order + 0.5 : item.order - 0.5;
|
|
89
|
+
|
|
90
|
+
onItemReOrder({ ...hoveringItem }, newOrder);
|
|
91
|
+
},
|
|
92
|
+
collect: monitor => ({
|
|
93
|
+
isOverCurrent: monitor.isOver({ shallow: true }),
|
|
94
|
+
})
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const [{ isDragging }, drag, dragPreview] = useDrag({
|
|
98
|
+
type: `${ItemTypes.NAVIGATION_ITEM}_${levelPath}`,
|
|
99
|
+
item: () => {
|
|
100
|
+
return { ...item };
|
|
101
|
+
},
|
|
102
|
+
collect: monitor => ({
|
|
103
|
+
isDragging: monitor.isDragging(),
|
|
104
|
+
}),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const refs = {
|
|
108
|
+
dragRef: drag(dragRef),
|
|
109
|
+
dropRef: drop(dropRef),
|
|
110
|
+
previewRef: dragPreview(previewRef),
|
|
111
|
+
}
|
|
112
|
+
|
|
63
113
|
return (
|
|
64
|
-
<Wrapper level={level} isLast={isLast}>
|
|
114
|
+
<Wrapper level={level} isLast={isLast} style={{ opacity: isDragging ? 0.2 : 1 }} ref={refs ? refs.dropRef : null} >
|
|
65
115
|
<Card style={{ width: "728px", zIndex: 1, position: "relative", overflow: 'hidden' }}>
|
|
66
|
-
{
|
|
67
|
-
<
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
<
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
116
|
+
{removed && (<ItemCardRemovedOverlay />)}
|
|
117
|
+
<div ref={refs.previewRef}>
|
|
118
|
+
<CardBody>
|
|
119
|
+
<ItemCardHeader
|
|
120
|
+
title={title}
|
|
121
|
+
path={isExternal ? externalPath : absolutePath}
|
|
122
|
+
icon={isExternal ? Earth : LinkIcon}
|
|
123
|
+
onItemRemove={() => onItemRemove({
|
|
124
|
+
...item,
|
|
125
|
+
relatedRef,
|
|
126
|
+
})}
|
|
127
|
+
onItemEdit={() => onItemEdit({
|
|
128
|
+
...item,
|
|
129
|
+
isMenuAllowedLevel,
|
|
130
|
+
isParentAttachedToMenu,
|
|
131
|
+
}, levelPath, isParentAttachedToMenu)}
|
|
132
|
+
onItemRestore={() => onItemRestore({
|
|
133
|
+
...item,
|
|
134
|
+
relatedRef,
|
|
135
|
+
})}
|
|
136
|
+
dragRef={refs.dragRef}
|
|
137
|
+
removed={removed}
|
|
138
|
+
/>
|
|
139
|
+
</CardBody>
|
|
140
|
+
<Divider />
|
|
141
|
+
{!isExternal && (<CardBody style={{ margin: '8px' }}>
|
|
142
|
+
<Flex style={{ width: '100%' }} direction="row" alignItems="center" justifyContent="space-between">
|
|
143
|
+
<TextButton
|
|
144
|
+
disabled={removed}
|
|
145
|
+
startIcon={<Plus />}
|
|
146
|
+
onClick={(e) => onItemLevelAdd(e, viewId, isNextMenuAllowedLevel, absolutePath, menuAttached)}
|
|
147
|
+
>
|
|
148
|
+
<Typography variant="pi" fontWeight="bold" textColor={removed ? "neutral600" : "primary600"}>
|
|
149
|
+
{getMessage("components.navigationItem.action.newItem")}
|
|
150
|
+
</Typography>
|
|
151
|
+
</TextButton>
|
|
152
|
+
{relatedItemLabel && (<Box>
|
|
101
153
|
<ItemCardBadge
|
|
102
154
|
borderColor={`${relatedBadgeColor}200`}
|
|
103
155
|
backgroundColor={`${relatedBadgeColor}100`}
|
|
104
156
|
textColor={`${relatedBadgeColor}600`}
|
|
105
157
|
className="action"
|
|
106
158
|
small
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
159
|
+
>
|
|
160
|
+
{getMessage({
|
|
161
|
+
id: `components.navigationItem.badge.${isPublished ? 'published' : 'draft'}`, props: {
|
|
162
|
+
type: relatedTypeLabel
|
|
163
|
+
}
|
|
164
|
+
})}
|
|
113
165
|
</ItemCardBadge>
|
|
114
166
|
<Typography variant="pi" fontWeight="bold" textColor="neutral600">
|
|
115
|
-
{
|
|
116
|
-
<Link
|
|
117
|
-
to={`/content-manager/collectionType/${relatedRef?.__collectionUid}/${relatedRef?.id}`}
|
|
167
|
+
{relatedItemLabel}
|
|
168
|
+
<Link
|
|
169
|
+
to={`/content-manager/collectionType/${relatedRef?.__collectionUid}/${relatedRef?.id}`}
|
|
118
170
|
endIcon={<ArrowRight />}> </Link>
|
|
119
171
|
</Typography>
|
|
120
172
|
</Box>)
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
173
|
+
}
|
|
174
|
+
</Flex>
|
|
175
|
+
</CardBody>)}
|
|
176
|
+
</div>
|
|
124
177
|
</Card>
|
|
125
178
|
{hasChildren && !removed && <List
|
|
126
179
|
onItemLevelAdd={onItemLevelAdd}
|
|
127
180
|
onItemRemove={onItemRemove}
|
|
128
181
|
onItemEdit={onItemEdit}
|
|
129
182
|
onItemRestore={onItemRestore}
|
|
183
|
+
onItemReOrder={onItemReOrder}
|
|
130
184
|
error={error}
|
|
131
185
|
allowedLevels={allowedLevels}
|
|
132
186
|
isParentAttachedToMenu={menuAttached}
|
|
@@ -159,9 +213,10 @@ Item.propTypes = {
|
|
|
159
213
|
onItemRestore: PropTypes.func.isRequired,
|
|
160
214
|
onItemLevelAdd: PropTypes.func.isRequired,
|
|
161
215
|
onItemRemove: PropTypes.func.isRequired,
|
|
216
|
+
onItemReOrder: PropTypes.func.isRequired,
|
|
162
217
|
config: PropTypes.shape({
|
|
163
|
-
contentTypes: PropTypes.array.isRequired,
|
|
164
|
-
contentTypesNameFields: PropTypes.object.isRequired,
|
|
218
|
+
contentTypes: PropTypes.array.isRequired,
|
|
219
|
+
contentTypesNameFields: PropTypes.object.isRequired,
|
|
165
220
|
}).isRequired
|
|
166
221
|
};
|
|
167
222
|
|
|
@@ -15,6 +15,7 @@ const List = ({
|
|
|
15
15
|
onItemLevelAdd,
|
|
16
16
|
onItemRemove,
|
|
17
17
|
onItemRestore,
|
|
18
|
+
onItemReOrder,
|
|
18
19
|
displayFlat,
|
|
19
20
|
contentTypes,
|
|
20
21
|
contentTypesNameFields,
|
|
@@ -36,6 +37,7 @@ const List = ({
|
|
|
36
37
|
onItemLevelAdd={onItemLevelAdd}
|
|
37
38
|
onItemRemove={onItemRemove}
|
|
38
39
|
onItemEdit={onItemEdit}
|
|
40
|
+
onItemReOrder={onItemReOrder}
|
|
39
41
|
error={error}
|
|
40
42
|
displayChildren={displayFlat}
|
|
41
43
|
config={{
|
|
@@ -57,6 +59,7 @@ List.propTypes = {
|
|
|
57
59
|
onItemRemove: PropTypes.func.isRequired,
|
|
58
60
|
onItemRestore: PropTypes.func.isRequired,
|
|
59
61
|
onItemRestore: PropTypes.func.isRequired,
|
|
62
|
+
onItemReOrder: PropTypes.func.isRequired,
|
|
60
63
|
contentTypes: PropTypes.array.isRequired,
|
|
61
64
|
contentTypesNameFields: PropTypes.object.isRequired
|
|
62
65
|
};
|
|
@@ -29,7 +29,7 @@ const Search = ({ value, setValue }) => {
|
|
|
29
29
|
onChange={(e) => setValue(e.target.value)}
|
|
30
30
|
clearLabel="Clearing the search"
|
|
31
31
|
placeholder={formatMessage({
|
|
32
|
-
id: getTradId('
|
|
32
|
+
id: getTradId('pages.main.search.placeholder'),
|
|
33
33
|
defaultMessage: 'Type to start searching...',
|
|
34
34
|
})}
|
|
35
35
|
>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
2
|
import { Formik } from 'formik';
|
|
3
|
-
import { isEmpty, capitalize, isEqual } from 'lodash';
|
|
3
|
+
import { isEmpty, capitalize, isEqual, orderBy } from 'lodash';
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
6
|
CheckPermissions,
|
|
@@ -8,9 +8,11 @@ import {
|
|
|
8
8
|
Form,
|
|
9
9
|
useOverlayBlocker,
|
|
10
10
|
useAutoReloadOverlayBlocker,
|
|
11
|
+
SettingsPageTitle,
|
|
11
12
|
} from '@strapi/helper-plugin';
|
|
12
13
|
import { Main } from '@strapi/design-system/Main';
|
|
13
14
|
import { ContentLayout, HeaderLayout } from '@strapi/design-system/Layout';
|
|
15
|
+
import { Accordion, AccordionToggle, AccordionContent, AccordionGroup } from '@strapi/design-system/Accordion';
|
|
14
16
|
import { Button } from '@strapi/design-system/Button';
|
|
15
17
|
import { Box } from '@strapi/design-system/Box';
|
|
16
18
|
import { Stack } from '@strapi/design-system/Stack';
|
|
@@ -19,13 +21,8 @@ import { Grid, GridItem } from '@strapi/design-system/Grid';
|
|
|
19
21
|
import { ToggleInput } from '@strapi/design-system/ToggleInput';
|
|
20
22
|
import { NumberInput } from '@strapi/design-system/NumberInput';
|
|
21
23
|
import { Select, Option } from '@strapi/design-system/Select';
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
Card,
|
|
26
|
-
CardBody,
|
|
27
|
-
CardContent,
|
|
28
|
-
} from '@strapi/design-system/Card';
|
|
24
|
+
import { Tooltip } from '@strapi/design-system/Tooltip';
|
|
25
|
+
import { Check, Refresh, Play, Information } from '@strapi/icons';
|
|
29
26
|
|
|
30
27
|
import permissions from '../../permissions';
|
|
31
28
|
import useNavigationConfig from '../../hooks/useNavigationConfig';
|
|
@@ -40,11 +37,18 @@ const SettingsPage = () => {
|
|
|
40
37
|
const { lockAppWithAutoreload, unlockAppWithAutoreload } = useAutoReloadOverlayBlocker();
|
|
41
38
|
const [isRestorePopupOpen, setIsRestorePopupOpen] = useState(false);
|
|
42
39
|
const [isRestartRequired, setIsRestartRequired] = useState(false);
|
|
40
|
+
const [contentTypeExpanded, setContentTypeExpanded] = useState(undefined);
|
|
43
41
|
const { data: navigationConfigData, isLoading: isConfigLoading, err: configErr, submitMutation, restoreMutation, restartMutation } = useNavigationConfig();
|
|
44
42
|
const { data: allContentTypesData, isLoading: isContentTypesLoading, err: contentTypesErr } = useAllContentTypes();
|
|
45
43
|
const isLoading = isConfigLoading || isContentTypesLoading;
|
|
46
44
|
const isError = configErr || contentTypesErr;
|
|
47
|
-
|
|
45
|
+
const boxDefaultProps = {
|
|
46
|
+
background: "neutral0",
|
|
47
|
+
hasRadius: true,
|
|
48
|
+
shadow: "filterShadow",
|
|
49
|
+
padding: 6,
|
|
50
|
+
};
|
|
51
|
+
|
|
48
52
|
const preparePayload = ({ selectedContentTypes, nameFields, audienceFieldChecked, allowedLevels }) => ({
|
|
49
53
|
contentTypes: selectedContentTypes,
|
|
50
54
|
contentTypesNameFields: nameFields,
|
|
@@ -53,7 +57,8 @@ const SettingsPage = () => {
|
|
|
53
57
|
gql: {
|
|
54
58
|
navigationItemRelated: selectedContentTypes.map(uid => allContentTypes.find(ct => ct.uid === uid).info.displayName)
|
|
55
59
|
}
|
|
56
|
-
})
|
|
60
|
+
});
|
|
61
|
+
|
|
57
62
|
const onSave = async (form) => {
|
|
58
63
|
lockApp();
|
|
59
64
|
const payload = preparePayload(form);
|
|
@@ -82,6 +87,7 @@ const SettingsPage = () => {
|
|
|
82
87
|
unlockAppWithAutoreload();
|
|
83
88
|
};
|
|
84
89
|
const handleRestartDiscard = () => setIsRestartRequired(false);
|
|
90
|
+
const handleSetContentTypeExpanded = key => setContentTypeExpanded(key === contentTypeExpanded ? undefined : key);
|
|
85
91
|
|
|
86
92
|
const prepareNameFieldFor = (uid, current, value) => ({
|
|
87
93
|
...current,
|
|
@@ -145,12 +151,7 @@ const SettingsPage = () => {
|
|
|
145
151
|
onClose={handleRestartDiscard}>
|
|
146
152
|
{getMessage('pages.settings.actions.restart.alert.description')}
|
|
147
153
|
</RestartAlert>)}
|
|
148
|
-
<Box
|
|
149
|
-
background="neutral0"
|
|
150
|
-
hasRadius
|
|
151
|
-
shadow="filterShadow"
|
|
152
|
-
padding={6}
|
|
153
|
-
>
|
|
154
|
+
<Box {...boxDefaultProps} >
|
|
154
155
|
<Stack size={4}>
|
|
155
156
|
<Typography variant="delta" as="h2">
|
|
156
157
|
{getMessage('pages.settings.general.title')}
|
|
@@ -172,31 +173,69 @@ const SettingsPage = () => {
|
|
|
172
173
|
{allContentTypes.map((item) => <Option key={item.uid} value={item.uid}>{item.info.displayName}</Option>)}
|
|
173
174
|
</Select>
|
|
174
175
|
</GridItem>
|
|
175
|
-
|
|
176
|
-
<
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
176
|
+
{!isEmpty(values.selectedContentTypes) && (
|
|
177
|
+
<GridItem col={12}>
|
|
178
|
+
<AccordionGroup
|
|
179
|
+
label={getMessage('pages.settings.form.contentTypesSettings.label')}
|
|
180
|
+
labelAction={<Tooltip description={getMessage('pages.settings.form.contentTypesSettings.tooltip')}>
|
|
181
|
+
<Information aria-hidden={true} />
|
|
182
|
+
</Tooltip>}>
|
|
183
|
+
{orderBy(values.selectedContentTypes).map(uid => {
|
|
184
|
+
const { attributes, info: { displayName } } = allContentTypes.find(item => item.uid == uid);
|
|
185
|
+
const stringAttributes = Object.keys(attributes).filter(_ => attributes[_].type === 'string');
|
|
186
|
+
const key = `collectionSettings-${uid}`;
|
|
187
|
+
return (<Accordion
|
|
188
|
+
expanded={contentTypeExpanded === key}
|
|
189
|
+
toggle={() => handleSetContentTypeExpanded(key)}
|
|
190
|
+
key={key}
|
|
191
|
+
id={key}
|
|
192
|
+
size="S">
|
|
193
|
+
<AccordionToggle title={displayName} togglePosition="left" />
|
|
194
|
+
<AccordionContent>
|
|
195
|
+
<Box padding={6}>
|
|
196
|
+
<Stack size={4}>
|
|
197
|
+
<Select
|
|
198
|
+
name={`collectionSettings-${uid}-entryLabel`}
|
|
199
|
+
label={getMessage('pages.settings.form.nameField.label')}
|
|
200
|
+
hint={getMessage('pages.settings.form.nameField.hint')}
|
|
201
|
+
placeholder={getMessage('pages.settings.form.nameField.placeholder')}
|
|
202
|
+
onClear={() => null}
|
|
203
|
+
value={values.nameFields[uid] || []}
|
|
204
|
+
onChange={(value) => setFieldValue('nameFields', prepareNameFieldFor(uid, values.nameFields, value))}
|
|
205
|
+
multi
|
|
206
|
+
withTags
|
|
207
|
+
disabled={isRestartRequired}
|
|
208
|
+
>
|
|
209
|
+
{stringAttributes.map(key =>
|
|
210
|
+
(<Option key={uid + key} value={key}>{capitalize(key.split('_').join(' '))}</Option>))}
|
|
211
|
+
</Select>
|
|
212
|
+
</Stack>
|
|
213
|
+
</Box>
|
|
214
|
+
</AccordionContent>
|
|
215
|
+
</Accordion>);
|
|
216
|
+
})}
|
|
217
|
+
</AccordionGroup>
|
|
218
|
+
</GridItem>)}
|
|
186
219
|
</Grid>
|
|
187
220
|
</Stack>
|
|
188
221
|
</Box>
|
|
189
|
-
<Box
|
|
190
|
-
background="neutral0"
|
|
191
|
-
hasRadius
|
|
192
|
-
shadow="filterShadow"
|
|
193
|
-
padding={6}
|
|
194
|
-
>
|
|
222
|
+
<Box {...boxDefaultProps} >
|
|
195
223
|
<Stack size={4}>
|
|
196
224
|
<Typography variant="delta" as="h2">
|
|
197
225
|
{getMessage('pages.settings.additional.title')}
|
|
198
226
|
</Typography>
|
|
199
227
|
<Grid gap={4}>
|
|
228
|
+
<GridItem col={3} s={6} xs={12}>
|
|
229
|
+
<NumberInput
|
|
230
|
+
name="allowedLevels"
|
|
231
|
+
label={getMessage('pages.settings.form.allowedLevels.label')}
|
|
232
|
+
placeholder={getMessage('pages.settings.form.allowedLevels.placeholder')}
|
|
233
|
+
hint={getMessage('pages.settings.form.allowedLevels.hint')}
|
|
234
|
+
onValueChange={(value) => setFieldValue('allowedLevels', value, false)}
|
|
235
|
+
value={values.allowedLevels}
|
|
236
|
+
disabled={isRestartRequired}
|
|
237
|
+
/>
|
|
238
|
+
</GridItem>
|
|
200
239
|
<GridItem col={6} s={12} xs={12}>
|
|
201
240
|
<ToggleInput
|
|
202
241
|
name="audienceFieldChecked"
|
|
@@ -212,62 +251,7 @@ const SettingsPage = () => {
|
|
|
212
251
|
</Grid>
|
|
213
252
|
</Stack>
|
|
214
253
|
</Box>
|
|
215
|
-
{
|
|
216
|
-
<Box
|
|
217
|
-
background="neutral0"
|
|
218
|
-
hasRadius
|
|
219
|
-
shadow="filterShadow"
|
|
220
|
-
padding={6}
|
|
221
|
-
>
|
|
222
|
-
<Stack size={4}>
|
|
223
|
-
<Typography variant="delta" as="h2">
|
|
224
|
-
{getMessage('pages.settings.nameField.title')}
|
|
225
|
-
</Typography>
|
|
226
|
-
<Grid gap={4}>
|
|
227
|
-
{values.selectedContentTypes.map(uid => {
|
|
228
|
-
const { attributes, info: { displayName } } = allContentTypes.find(item => item.uid == uid);
|
|
229
|
-
const stringAttributes = Object.keys(attributes).filter(_ => attributes[_].type === 'string');
|
|
230
|
-
|
|
231
|
-
return !isEmpty(stringAttributes) && (
|
|
232
|
-
<GridItem key={`collectionSettings-${uid}`} col={6} s={12} xs={12}>
|
|
233
|
-
<Card background="primary100" borderColor="primary200">
|
|
234
|
-
<CardBody>
|
|
235
|
-
<CardContent style={{ width: '100%' }}>
|
|
236
|
-
<Stack size={4}>
|
|
237
|
-
<Typography variant="epsilon" fontWeight="semibold" as="h3">{displayName}</Typography>
|
|
238
|
-
<Select
|
|
239
|
-
name={`collectionSettings-${uid}-entryLabel`}
|
|
240
|
-
label={getMessage('pages.settings.form.nameField.label')}
|
|
241
|
-
hint={getMessage('pages.settings.form.nameField.hint')}
|
|
242
|
-
placeholder={getMessage('pages.settings.form.nameField.placeholder')}
|
|
243
|
-
onClear={() => null}
|
|
244
|
-
value={values.nameFields[uid] || []}
|
|
245
|
-
onChange={(value) => setFieldValue('nameFields', prepareNameFieldFor(uid, values.nameFields, value))}
|
|
246
|
-
multi
|
|
247
|
-
withTags
|
|
248
|
-
disabled={isRestartRequired}
|
|
249
|
-
>
|
|
250
|
-
{stringAttributes.map(key =>
|
|
251
|
-
(<Option key={uid + key} value={key}>{capitalize(key.split('_').join(' '))}</Option>))}
|
|
252
|
-
</Select>
|
|
253
|
-
</Stack>
|
|
254
|
-
</CardContent>
|
|
255
|
-
</CardBody>
|
|
256
|
-
</Card>
|
|
257
|
-
</GridItem>
|
|
258
|
-
);
|
|
259
|
-
})
|
|
260
|
-
}
|
|
261
|
-
</Grid>
|
|
262
|
-
</Stack>
|
|
263
|
-
</Box>
|
|
264
|
-
)}
|
|
265
|
-
<Box
|
|
266
|
-
background="neutral0"
|
|
267
|
-
hasRadius
|
|
268
|
-
shadow="filterShadow"
|
|
269
|
-
padding={6}
|
|
270
|
-
>
|
|
254
|
+
<Box {...boxDefaultProps} >
|
|
271
255
|
<Stack size={4}>
|
|
272
256
|
<Typography variant="delta" as="h2">
|
|
273
257
|
{getMessage('pages.settings.restoring.title')}
|
|
@@ -80,8 +80,10 @@ const NavigationItemForm = ({
|
|
|
80
80
|
const sanitizedType = purePayload.type || navigationItemType.INTERNAL;
|
|
81
81
|
const relatedId = related
|
|
82
82
|
const relatedCollectionType = relatedType;
|
|
83
|
+
const title = payload.title || relatedSelectOptions.find(v => v.key == relatedId)?.label
|
|
83
84
|
return {
|
|
84
85
|
...purePayload,
|
|
86
|
+
title,
|
|
85
87
|
menuAttached: isNil(menuAttached) ? false : menuAttached,
|
|
86
88
|
type: sanitizedType,
|
|
87
89
|
path: sanitizedType === navigationItemType.INTERNAL ? purePayload.path : undefined,
|
|
@@ -164,8 +166,8 @@ const NavigationItemForm = ({
|
|
|
164
166
|
key: get(item, 'id'),
|
|
165
167
|
metadatas: {
|
|
166
168
|
intlLabel: {
|
|
167
|
-
id: label ||
|
|
168
|
-
defaultMessage: label ||
|
|
169
|
+
id: label || `${item.__collectionUid} ${item.id}`,
|
|
170
|
+
defaultMessage: label || `${item.__collectionUid} ${item.id}`,
|
|
169
171
|
}
|
|
170
172
|
},
|
|
171
173
|
value: item.id,
|
|
@@ -120,6 +120,13 @@ const View = () => {
|
|
|
120
120
|
}, []);
|
|
121
121
|
const filteredList = !isSearchEmpty ? filteredListFactory(changedActiveNavigation.items, (item) => item?.title.includes(searchValue)) : [];
|
|
122
122
|
|
|
123
|
+
const handleItemReOrder = (item, newOrder) => {
|
|
124
|
+
handleSubmitNavigationItem({
|
|
125
|
+
...item,
|
|
126
|
+
order: newOrder,
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
123
130
|
const handleItemRemove = (item) => {
|
|
124
131
|
handleSubmitNavigationItem({
|
|
125
132
|
...item,
|
|
@@ -206,6 +213,7 @@ const View = () => {
|
|
|
206
213
|
onItemRemove={handleItemRemove}
|
|
207
214
|
onItemEdit={handleItemEdit}
|
|
208
215
|
onItemRestore={handleItemRestore}
|
|
216
|
+
onItemReOrder={handleItemReOrder}
|
|
209
217
|
displayFlat={!isSearchEmpty}
|
|
210
218
|
root
|
|
211
219
|
error={error}
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"notification.navigation.item.relation": "Entity relation does not exist!",
|
|
44
44
|
"notification.navigation.item.relation.status.draft": "draft",
|
|
45
45
|
"notification.navigation.item.relation.status.published": "published",
|
|
46
|
+
"pages.main.search.placeholder": "Type to start searching...",
|
|
46
47
|
"pages.settings.general.title": "General settings",
|
|
47
48
|
"pages.settings.additional.title": "Additional settings",
|
|
48
49
|
"pages.settings.nameField.title": "Content types settings",
|
|
@@ -80,6 +81,8 @@
|
|
|
80
81
|
"pages.settings.form.nameField.label": "Name fields",
|
|
81
82
|
"pages.settings.form.nameField.placeholder": "Select at least one or leave empty to apply defaults",
|
|
82
83
|
"pages.settings.form.nameField.hint": "If left empty name field is going to take following ordered fields: \"title\", \"subject\" and \"name\"",
|
|
84
|
+
"pages.settings.form.contentTypesSettings.label": "Content types",
|
|
85
|
+
"pages.settings.form.contentTypesSettings.tooltip": "Custom configuration per content type",
|
|
83
86
|
"components.navigationItem.action.newItem": "Add nested item",
|
|
84
87
|
"components.navigationItem.badge.removed": "Removed",
|
|
85
88
|
"components.navigationItem.badge.draft": "{type}: Draft",
|
package/admin/src/utils/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { isString } from 'lodash';
|
|
|
3
3
|
|
|
4
4
|
import pluginId from '../pluginId';
|
|
5
5
|
|
|
6
|
-
const getMessage = (input, defaultMessage = '', inPluginScope = true) => {
|
|
6
|
+
export const getMessage = (input, defaultMessage = '', inPluginScope = true) => {
|
|
7
7
|
const { formatMessage } = useIntl();
|
|
8
8
|
let formattedId = ''
|
|
9
9
|
if (isString(input)) {
|
|
@@ -17,4 +17,6 @@ const getMessage = (input, defaultMessage = '', inPluginScope = true) => {
|
|
|
17
17
|
}, input?.props || undefined)
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
-
export
|
|
20
|
+
export const ItemTypes = {
|
|
21
|
+
NAVIGATION_ITEM: 'navigationItem'
|
|
22
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "strapi-plugin-navigation",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.5",
|
|
4
4
|
"description": "Strapi - Navigation plugin",
|
|
5
5
|
"strapi": {
|
|
6
6
|
"name": "navigation",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"test:unit": "jest --verbose --coverage"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@strapi/utils": "^4.0
|
|
20
|
+
"@strapi/utils": "^4.1.0",
|
|
21
21
|
"uuid": "^8.3.0",
|
|
22
22
|
"bad-words": "^3.0.3",
|
|
23
23
|
"lodash": "^4.17.11",
|
|
@@ -74,13 +74,14 @@ module.exports = ({strapi}) => ({
|
|
|
74
74
|
},
|
|
75
75
|
async render(ctx) {
|
|
76
76
|
const { params, query = {} } = ctx;
|
|
77
|
-
const { type, menu: menuOnly } = query;
|
|
77
|
+
const { type, menu: menuOnly, path: rootPath } = query;
|
|
78
78
|
const { idOrSlug } = parseParams(params);
|
|
79
|
-
return getService().render(
|
|
79
|
+
return getService().render({
|
|
80
80
|
idOrSlug,
|
|
81
81
|
type,
|
|
82
82
|
menuOnly,
|
|
83
|
-
|
|
83
|
+
rootPath
|
|
84
|
+
});
|
|
84
85
|
},
|
|
85
86
|
async renderChild(ctx) {
|
|
86
87
|
const { params, query = {} } = ctx;
|
|
@@ -5,11 +5,12 @@ module.exports = ({ strapi, nexus }) => {
|
|
|
5
5
|
args: {
|
|
6
6
|
navigationIdOrSlug: nonNull(stringArg()),
|
|
7
7
|
type: 'NavigationRenderType',
|
|
8
|
-
menuOnly: booleanArg()
|
|
8
|
+
menuOnly: booleanArg(),
|
|
9
|
+
path: stringArg(),
|
|
9
10
|
},
|
|
10
11
|
resolve(obj, args) {
|
|
11
|
-
const { navigationIdOrSlug, type, menuOnly } = args;
|
|
12
|
-
return strapi.plugin('navigation').service('navigation').render(
|
|
12
|
+
const { navigationIdOrSlug: idOrSlug, type, menuOnly, path: rootPath } = args;
|
|
13
|
+
return strapi.plugin('navigation').service('navigation').render({idOrSlug, type, menuOnly, rootPath});
|
|
13
14
|
},
|
|
14
15
|
};
|
|
15
16
|
}
|
|
@@ -13,7 +13,7 @@ module.exports = ({ nexus }) =>
|
|
|
13
13
|
t.int("parent")
|
|
14
14
|
t.int("master")
|
|
15
15
|
t.list.field("items", { type: 'NavigationItem' })
|
|
16
|
-
t.
|
|
16
|
+
t.field("related", { type: 'NavigationRelated' })
|
|
17
17
|
t.list.string("audience")
|
|
18
18
|
// SQL
|
|
19
19
|
t.string("created_at")
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const { setupStrapi } = require('../../../__mocks__/strapi');
|
|
2
|
+
const utilsFunctionsFactory = require('../utils/functions');
|
|
3
|
+
|
|
4
|
+
describe('Utilities functions', () => {
|
|
5
|
+
beforeAll(async () => {
|
|
6
|
+
setupStrapi();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
describe('Path rendering functions', () => {
|
|
10
|
+
it('Can build nested path structure', async () => {
|
|
11
|
+
const utilsFunctions = utilsFunctionsFactory({ strapi });
|
|
12
|
+
const { itemModel } = utilsFunctions.extractMeta(strapi.plugins);
|
|
13
|
+
const rootPath = '/home/side';
|
|
14
|
+
const entities = await strapi
|
|
15
|
+
.query(itemModel.uid)
|
|
16
|
+
.findMany({
|
|
17
|
+
where: {
|
|
18
|
+
master: 1
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
const nested = utilsFunctions.buildNestedPaths({ items: entities });
|
|
22
|
+
|
|
23
|
+
expect(nested.length).toBe(2);
|
|
24
|
+
expect(nested[1].path).toBe(rootPath);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('Can filter items by path', async () => {
|
|
28
|
+
const utilsFunctions = utilsFunctionsFactory({ strapi });
|
|
29
|
+
const { itemModel } = utilsFunctions.extractMeta(strapi.plugins);
|
|
30
|
+
const rootPath = '/home/side';
|
|
31
|
+
const entities = await strapi
|
|
32
|
+
.query(itemModel.uid)
|
|
33
|
+
.findMany({
|
|
34
|
+
where: {
|
|
35
|
+
master: 1
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
const {
|
|
39
|
+
root,
|
|
40
|
+
items
|
|
41
|
+
} = utilsFunctions.filterByPath(entities, rootPath);
|
|
42
|
+
|
|
43
|
+
expect(root).toBeDefined();
|
|
44
|
+
expect(root.path).toBe(rootPath);
|
|
45
|
+
expect(items.length).toBe(1)
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -26,7 +26,7 @@ describe('Navigation services', () => {
|
|
|
26
26
|
describe('Render navigation', () => {
|
|
27
27
|
it('Can render branch in flat format', async () => {
|
|
28
28
|
const service = strapi.plugin('navigation').service('navigation');
|
|
29
|
-
const result = await service.render(1);
|
|
29
|
+
const result = await service.render({ idOrSlug: 1 });
|
|
30
30
|
|
|
31
31
|
expect(result).toBeDefined()
|
|
32
32
|
expect(result.length).toBe(2)
|
|
@@ -34,7 +34,10 @@ describe('Navigation services', () => {
|
|
|
34
34
|
|
|
35
35
|
it('Can render branch in tree format', async () => {
|
|
36
36
|
const service = strapi.plugin('navigation').service('navigation');
|
|
37
|
-
const result = await service.render(
|
|
37
|
+
const result = await service.render({
|
|
38
|
+
idOrSlug: 1,
|
|
39
|
+
type: "TREE"
|
|
40
|
+
});
|
|
38
41
|
|
|
39
42
|
expect(result).toBeDefined()
|
|
40
43
|
expect(result.length).toBeGreaterThan(0)
|
|
@@ -42,7 +45,10 @@ describe('Navigation services', () => {
|
|
|
42
45
|
|
|
43
46
|
it('Can render branch in rfr format', async () => {
|
|
44
47
|
const service = strapi.plugin('navigation').service('navigation');
|
|
45
|
-
const result = await service.render(
|
|
48
|
+
const result = await service.render({
|
|
49
|
+
idOrSlug: 1,
|
|
50
|
+
type: "RFR"
|
|
51
|
+
});
|
|
46
52
|
|
|
47
53
|
expect(result).toBeDefined()
|
|
48
54
|
expect(result.length).toBeGreaterThan(0)
|
|
@@ -50,11 +56,27 @@ describe('Navigation services', () => {
|
|
|
50
56
|
|
|
51
57
|
it('Can render only menu attached elements', async () => {
|
|
52
58
|
const service = strapi.plugin('navigation').service('navigation');
|
|
53
|
-
const result = await service.render(
|
|
59
|
+
const result = await service.render({
|
|
60
|
+
idOrSlug: 1,
|
|
61
|
+
type: "FLAT",
|
|
62
|
+
menuOnly: true.valueOf,
|
|
63
|
+
});
|
|
54
64
|
|
|
55
65
|
expect(result).toBeDefined()
|
|
56
66
|
expect(result.length).toBe(1)
|
|
57
67
|
});
|
|
68
|
+
|
|
69
|
+
it('Can render branch by path', async () => {
|
|
70
|
+
const service = strapi.plugin('navigation').service('navigation');
|
|
71
|
+
const result = await service.render({
|
|
72
|
+
idOrSlug: 1,
|
|
73
|
+
type: "FLAT",
|
|
74
|
+
rootPath: '/home/side'
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
expect(result).toBeDefined();
|
|
78
|
+
expect(result.length).toBe(1);
|
|
79
|
+
});
|
|
58
80
|
});
|
|
59
81
|
|
|
60
82
|
describe('Render child', () => {
|
|
@@ -66,7 +66,7 @@ module.exports = ({ strapi }) => {
|
|
|
66
66
|
},
|
|
67
67
|
|
|
68
68
|
async restart() {
|
|
69
|
-
|
|
69
|
+
setImmediate(() => strapi.reload());
|
|
70
70
|
},
|
|
71
71
|
|
|
72
72
|
// Get plugin config
|
|
@@ -342,20 +342,20 @@ module.exports = ({ strapi }) => {
|
|
|
342
342
|
...(type === renderType.FLAT ? { uiRouterKey: childUIKey } : {}),
|
|
343
343
|
};
|
|
344
344
|
|
|
345
|
-
return service.renderType(type, criteria, itemCriteria, filter);
|
|
345
|
+
return service.renderType({ type, criteria, itemCriteria, filter });
|
|
346
346
|
},
|
|
347
347
|
|
|
348
|
-
async render(idOrSlug, type = renderType.FLAT, menuOnly = false) {
|
|
348
|
+
async render({ idOrSlug, type = renderType.FLAT, menuOnly = false, rootPath = null }) {
|
|
349
349
|
const { service } = utilsFunctions.extractMeta(strapi.plugins);
|
|
350
350
|
|
|
351
351
|
const findById = !isNaN(toNumber(idOrSlug)) || isUuid(idOrSlug);
|
|
352
352
|
const criteria = findById ? { id: idOrSlug } : { slug: idOrSlug };
|
|
353
353
|
const itemCriteria = menuOnly ? { menuAttached: true } : {};
|
|
354
354
|
|
|
355
|
-
return service.renderType(type, criteria, itemCriteria);
|
|
355
|
+
return service.renderType({ type, criteria, itemCriteria, rootPath });
|
|
356
356
|
},
|
|
357
357
|
|
|
358
|
-
async renderType(type = renderType.FLAT, criteria = {}, itemCriteria = {}, filter = null) {
|
|
358
|
+
async renderType({ type = renderType.FLAT, criteria = {}, itemCriteria = {}, filter = null, rootPath = null }) {
|
|
359
359
|
const { pluginName, service, masterModel, itemModel } = utilsFunctions.extractMeta(
|
|
360
360
|
strapi.plugins,
|
|
361
361
|
);
|
|
@@ -393,8 +393,8 @@ module.exports = ({ strapi }) => {
|
|
|
393
393
|
const getTemplateName = await utilsFunctions.templateNameFactory(items, strapi, contentTypes);
|
|
394
394
|
const itemParser = (item, path = '', field) => {
|
|
395
395
|
const isExternal = item.type === itemType.EXTERNAL;
|
|
396
|
-
const parentPath = isExternal ? undefined : `${path === '/' ? '' : path}/${item.path === '/'
|
|
397
|
-
?
|
|
396
|
+
const parentPath = isExternal ? undefined : `${path === '/' ? '' : path}/${first(item.path) === '/'
|
|
397
|
+
? item.path.substring(1)
|
|
398
398
|
: item.path}`;
|
|
399
399
|
const slug = isString(parentPath) ? slugify(
|
|
400
400
|
(first(parentPath) === '/' ? parentPath.substring(1) : parentPath).replace(/\//g, '-')) : undefined;
|
|
@@ -422,9 +422,17 @@ module.exports = ({ strapi }) => {
|
|
|
422
422
|
}),
|
|
423
423
|
};
|
|
424
424
|
};
|
|
425
|
+
|
|
426
|
+
const {
|
|
427
|
+
items: itemsFilteredByPath,
|
|
428
|
+
root: rootElement,
|
|
429
|
+
} = utilsFunctions.filterByPath(items, rootPath);
|
|
430
|
+
|
|
425
431
|
const treeStructure = service.renderTree({
|
|
426
|
-
items,
|
|
432
|
+
items: isNil(rootPath) ? items : itemsFilteredByPath,
|
|
427
433
|
field: 'parent',
|
|
434
|
+
id: get(rootElement, 'parent.id'),
|
|
435
|
+
path: get(rootElement, 'parent.path'),
|
|
428
436
|
itemParser,
|
|
429
437
|
});
|
|
430
438
|
|
|
@@ -440,7 +448,7 @@ module.exports = ({ strapi }) => {
|
|
|
440
448
|
}
|
|
441
449
|
return filteredStructure;
|
|
442
450
|
default:
|
|
443
|
-
|
|
451
|
+
const publishedItems = items
|
|
444
452
|
.filter(utilsFunctions.filterOutUnpublished)
|
|
445
453
|
.map((item) => ({
|
|
446
454
|
...item,
|
|
@@ -449,6 +457,7 @@ module.exports = ({ strapi }) => {
|
|
|
449
457
|
related: item.related?.map(({ localizations, ...item }) => item),
|
|
450
458
|
items: null,
|
|
451
459
|
}));
|
|
460
|
+
return isNil(rootPath) ? items : utilsFunctions.filterByPath(publishedItems, rootPath).items;
|
|
452
461
|
}
|
|
453
462
|
}
|
|
454
463
|
throw new NotFoundError();
|
|
@@ -6,6 +6,8 @@ const {
|
|
|
6
6
|
find,
|
|
7
7
|
isString,
|
|
8
8
|
get,
|
|
9
|
+
isNil,
|
|
10
|
+
isArray
|
|
9
11
|
} = require('lodash');
|
|
10
12
|
|
|
11
13
|
const { type: itemType } = require('../../content-types/navigation-item/lifecycle');
|
|
@@ -52,6 +54,45 @@ module.exports = ({ strapi }) => {
|
|
|
52
54
|
});
|
|
53
55
|
},
|
|
54
56
|
|
|
57
|
+
buildNestedPaths({items, id = null, field = 'parent', parentPath = null}){
|
|
58
|
+
return items
|
|
59
|
+
.filter(entity => {
|
|
60
|
+
if (entity[field] == null && id === null) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
let data = entity[field];
|
|
64
|
+
if (data && typeof id === 'string') {
|
|
65
|
+
data = data.toString();
|
|
66
|
+
}
|
|
67
|
+
return (data && data === id) || (isObject(entity[field]) && (entity[field].id === id));
|
|
68
|
+
})
|
|
69
|
+
.reduce((acc, entity) => {
|
|
70
|
+
const path = `${parentPath || ''}/${entity.path}`
|
|
71
|
+
return [
|
|
72
|
+
{
|
|
73
|
+
id: entity.id,
|
|
74
|
+
parent: parentPath && {
|
|
75
|
+
id: get(entity, 'parent.id'),
|
|
76
|
+
path: parentPath,
|
|
77
|
+
},
|
|
78
|
+
path
|
|
79
|
+
},
|
|
80
|
+
...this.buildNestedPaths({items, id: entity.id, field, parentPath: path}),
|
|
81
|
+
...acc,
|
|
82
|
+
];
|
|
83
|
+
}, [])
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
filterByPath(items, path) {
|
|
87
|
+
const itemsWithPaths = this.buildNestedPaths({ items }).filter(({path: itemPath}) => itemPath.includes(path));
|
|
88
|
+
const root = itemsWithPaths.find(({ path: itemPath }) => itemPath === path);
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
root,
|
|
92
|
+
items: isNil(root) ? [] : items.filter(({ id }) => (itemsWithPaths.find(v => v.id === id))),
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
|
|
55
96
|
prepareAuditLog(actions) {
|
|
56
97
|
return [
|
|
57
98
|
...new Set(
|