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 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 size={2}>
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
- ${ props => props.small && `
8
- padding: ${props.theme.spaces[1]};
9
- margin: 0px ${props.theme.spaces[3]};
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: .55rem;
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
- <Icon as={icon} />
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: ${({ theme, level }) => level && theme.spaces[8]}};
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, useEffect } from 'react';
1
+ import React, { useRef } from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import { useDrag, useDrop } from 'react-dnd';
4
- import { getEmptyImage } from 'react-dnd-html5-backend';
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 isPublished = relatedRef && relatedRef?.publishedAt;
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 && (<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>
153
- <ItemCardBadge
154
- borderColor={`${relatedBadgeColor}200`}
155
- backgroundColor={`${relatedBadgeColor}100`}
156
- textColor={`${relatedBadgeColor}600`}
157
- className="action"
158
- small
159
- >
160
- {getMessage({
161
- id: `components.navigationItem.badge.${isPublished ? 'published' : 'draft'}`, props: {
162
- type: relatedTypeLabel
163
- }
164
- })}
165
- </ItemCardBadge>
166
- <Typography variant="pi" fontWeight="bold" textColor="neutral600">
167
- {relatedItemLabel}
168
- <Link
169
- to={`/content-manager/collectionType/${relatedRef?.__collectionUid}/${relatedRef?.id}`}
170
- endIcon={<ArrowRight />}>&nbsp;</Link>
171
- </Typography>
172
- </Box>)
173
- }
174
- </Flex>
175
- </CardBody>)}
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}&nbsp;/&nbsp;</Typography>
181
+ <Typography variant="omega" textColor='neutral800'>{relatedItemLabel}</Typography>
182
+ <Link
183
+ to={`/content-manager/collectionType/${relatedRef?.__collectionUid}/${relatedRef?.id}`}
184
+ endIcon={<ArrowRight />}>&nbsp;</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,
@@ -11,7 +11,7 @@ const Wrapper = styled.div`
11
11
 
12
12
  position: absolute;
13
13
  top: -${theme.spaces[2]};
14
- left: ${theme.spaces[4]};
14
+ left: 30px;
15
15
 
16
16
  border: 0px solid transparent;
17
17
  border-left: 4px solid ${theme.colors.neutral300};
@@ -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: 'error',
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: 'error',
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: 'error',
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: 'error',
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 size={7}>
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 size={4}>
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 size={4}>
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('pages.settings.form.nameField.hint')}
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 size={4}>
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
- <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>
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 size={4}>
275
+ <Stack spacing={4}>
256
276
  <Typography variant="delta" as="h2">
257
277
  {getMessage('pages.settings.restoring.title')}
258
278
  </Typography>
@@ -22,7 +22,7 @@ const NavigationHeader = ({
22
22
  return (
23
23
  <HeaderLayout
24
24
  primaryAction={
25
- <Stack horizontal size={2}>
25
+ <Stack horizontal spacing={2}>
26
26
  <Box width="10vw">
27
27
  <Select
28
28
  type="select"
@@ -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 = payload.title || relatedSelectOptions.find(v => v.key == relatedId)?.label
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: sanitizedType,
89
- path: sanitizedType === navigationItemType.INTERNAL ? purePayload.path : undefined,
90
- externalPath: sanitizedType === navigationItemType.EXTERNAL ? purePayload.externalPath : undefined,
91
- related: relatedId,
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(purePayload.title, relatedId, relatedCollectionType),
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 = (form.type !== navigationItemType.EXTERNAL) && isNil(form.related);
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: appendLabelPublicationStatus(get(item, 'label', get(item, 'name')), item, true),
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}menuAttached`} col={6} lg={12}>
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
- {!isExternal && (
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={<Button
184
- onClick={addNewNavigationItem}
185
- startIcon={<PlusIcon />}
186
- disabled={isLoadingForSubmit}
187
- type="submit"
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}
@@ -1,6 +1,7 @@
1
1
  export const navigationItemType = {
2
2
  INTERNAL: "INTERNAL",
3
3
  EXTERNAL: "EXTERNAL",
4
+ WRAPPER: "WRAPPER",
4
5
  };
5
6
 
6
7
  export const navigationItemAdditionalFields = {
@@ -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 === navigationItemType.EXTERNAL || (type === navigationItemType.INTERNAL && isRelationDefined);
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
- (item.removed || isRelationCorrect({
296
- related: item.related,
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": "Internal link",
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": "{type}: Draft",
89
- "components.navigationItem.badge.published": "{type}: 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strapi-plugin-navigation",
3
- "version": "2.0.6",
3
+ "version": "2.0.9",
4
4
  "description": "Strapi - Navigation plugin",
5
5
  "strapi": {
6
6
  "name": "navigation",
@@ -42,30 +42,11 @@ module.exports = async ({ strapi }) => {
42
42
  }
43
43
 
44
44
  // Initialize configuration
45
- const pluginStore = strapi.store({
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: config || defaultConfigValue });
50
+ await graphqlConfiguration({ strapi, config });
70
51
  }
71
52
  };
@@ -3,6 +3,7 @@ module.exports = {
3
3
  additionalFields: [],
4
4
  contentTypes: [],
5
5
  contentTypesNameFields: {},
6
+ contentTypesPopulate: {},
6
7
  allowedLevels: 2
7
8
  }
8
9
  }
package/server/config.js CHANGED
@@ -3,6 +3,7 @@ module.exports = {
3
3
  additionalFields: [],
4
4
  contentTypes: [],
5
5
  contentTypesNameFields: {},
6
+ contentTypesPopulate: {},
6
7
  allowedLevels: 2
7
8
  }
8
9
  }
@@ -10,6 +10,7 @@ module.exports = {
10
10
  type: {
11
11
  INTERNAL: 'INTERNAL',
12
12
  EXTERNAL: 'EXTERNAL',
13
+ WRAPPER: 'WRAPPER',
13
14
  },
14
15
  additionalFields: {
15
16
  AUDIENCE: 'audience',
@@ -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.store({ type: 'plugin', name: 'navigation' });
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
- paggination: {
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
- paggination: {
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.store({ type: 'plugin', name: 'navigation' });
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.store({ type: 'plugin', name: 'navigation' });
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.store({ type: 'plugin', name: 'navigation' });
120
- const defaultConfig = await strapi.plugin('navigation').config
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.store({ type: 'plugin', name: 'navigation' });
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 === itemType.EXTERNAL) || relatedItem;
217
+ return (item.type !== itemType.INTERNAL) || relatedItem;
217
218
  },
218
219
  };
219
220
  }