strapi-plugin-navigation 2.0.4 → 2.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/README.md +2 -1
  2. package/admin/src/components/CollapseButton/index.js +31 -0
  3. package/admin/src/components/Item/ItemCardBadge/index.js +4 -4
  4. package/admin/src/components/Item/ItemCardHeader/Wrapper.js +0 -4
  5. package/admin/src/components/Item/ItemCardHeader/index.js +5 -4
  6. package/admin/src/components/Item/Wrapper.js +1 -1
  7. package/admin/src/components/Item/index.js +129 -66
  8. package/admin/src/components/NavigationItemList/Wrapper.js +1 -1
  9. package/admin/src/components/NavigationItemList/index.js +6 -0
  10. package/admin/src/components/Search/index.js +1 -1
  11. package/admin/src/pages/SettingsPage/index.js +95 -91
  12. package/admin/src/pages/View/components/NavigationItemForm/index.js +49 -30
  13. package/admin/src/pages/View/components/NavigationItemPopup/NavigationItemPopupHeader.js +3 -3
  14. package/admin/src/pages/View/index.js +81 -9
  15. package/admin/src/pages/View/utils/enums.js +1 -0
  16. package/admin/src/pages/View/utils/parsers.js +6 -3
  17. package/admin/src/translations/en.json +15 -3
  18. package/admin/src/utils/index.js +4 -2
  19. package/package.json +2 -3
  20. package/server/bootstrap.js +3 -22
  21. package/server/config/index.js +1 -0
  22. package/server/config.js +1 -0
  23. package/server/content-types/navigation-item/lifecycle.js +1 -0
  24. package/server/content-types/navigation-item/schema.json +7 -1
  25. package/server/graphql/types/content-types-name-fields.js +2 -2
  26. package/server/graphql/types/navigation-item.js +1 -1
  27. package/server/services/navigation.js +44 -19
  28. package/server/services/utils/functions.js +1 -1
  29. package/yarn-error.log +0 -5263
@@ -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 { Check, Refresh, Play } from '@strapi/icons';
23
- import { SettingsPageTitle } from '@strapi/helper-plugin';
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,20 +37,29 @@ 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
-
48
- const preparePayload = ({ selectedContentTypes, nameFields, audienceFieldChecked, allowedLevels }) => ({
45
+ const boxDefaultProps = {
46
+ background: "neutral0",
47
+ hasRadius: true,
48
+ shadow: "filterShadow",
49
+ padding: 6,
50
+ };
51
+
52
+ const preparePayload = ({ selectedContentTypes, nameFields, audienceFieldChecked, allowedLevels, populate }) => ({
49
53
  contentTypes: selectedContentTypes,
50
54
  contentTypesNameFields: nameFields,
55
+ contentTypesPopulate: populate,
51
56
  additionalFields: audienceFieldChecked ? [navigationItemAdditionalFields.AUDIENCE] : [],
52
57
  allowedLevels: allowedLevels,
53
58
  gql: {
54
- navigationItemRelated: selectedContentTypes.map(uid => allContentTypes.find(ct => ct.uid === uid).info.displayName)
59
+ navigationItemRelated: selectedContentTypes.map(uid => allContentTypes.find(ct => ct.uid === uid).info.displayName.replace(/\s+/g, ''))
55
60
  }
56
- })
61
+ });
62
+
57
63
  const onSave = async (form) => {
58
64
  lockApp();
59
65
  const payload = preparePayload(form);
@@ -82,6 +88,7 @@ const SettingsPage = () => {
82
88
  unlockAppWithAutoreload();
83
89
  };
84
90
  const handleRestartDiscard = () => setIsRestartRequired(false);
91
+ const handleSetContentTypeExpanded = key => setContentTypeExpanded(key === contentTypeExpanded ? undefined : key);
85
92
 
86
93
  const prepareNameFieldFor = (uid, current, value) => ({
87
94
  ...current,
@@ -104,8 +111,9 @@ const SettingsPage = () => {
104
111
  const allContentTypes = !isLoading && Object.values(allContentTypesData).filter(item => item.uid.includes('api::'));
105
112
  const selectedContentTypes = navigationConfigData?.contentTypes.map(item => item.uid);
106
113
  const audienceFieldChecked = navigationConfigData?.additionalFields.includes(navigationItemAdditionalFields.AUDIENCE);
107
- const allowedLevels = navigationConfigData?.allowedLevels;
108
- const nameFields = navigationConfigData?.contentTypesNameFields
114
+ const allowedLevels = navigationConfigData?.allowedLevels || 2;
115
+ const nameFields = navigationConfigData?.contentTypesNameFields || {}
116
+ const populate = navigationConfigData?.contentTypesPopulate || {}
109
117
 
110
118
  return (
111
119
  <>
@@ -119,6 +127,7 @@ const SettingsPage = () => {
119
127
  audienceFieldChecked,
120
128
  allowedLevels,
121
129
  nameFields,
130
+ populate,
122
131
  }}
123
132
  onSubmit={onSave}
124
133
  >
@@ -145,12 +154,7 @@ const SettingsPage = () => {
145
154
  onClose={handleRestartDiscard}>
146
155
  {getMessage('pages.settings.actions.restart.alert.description')}
147
156
  </RestartAlert>)}
148
- <Box
149
- background="neutral0"
150
- hasRadius
151
- shadow="filterShadow"
152
- padding={6}
153
- >
157
+ <Box {...boxDefaultProps} >
154
158
  <Stack size={4}>
155
159
  <Typography variant="delta" as="h2">
156
160
  {getMessage('pages.settings.general.title')}
@@ -172,6 +176,75 @@ const SettingsPage = () => {
172
176
  {allContentTypes.map((item) => <Option key={item.uid} value={item.uid}>{item.info.displayName}</Option>)}
173
177
  </Select>
174
178
  </GridItem>
179
+ {!isEmpty(values.selectedContentTypes) && (
180
+ <GridItem col={12}>
181
+ <AccordionGroup
182
+ label={getMessage('pages.settings.form.contentTypesSettings.label')}
183
+ labelAction={<Tooltip description={getMessage('pages.settings.form.contentTypesSettings.tooltip')}>
184
+ <Information aria-hidden={true} />
185
+ </Tooltip>}>
186
+ {orderBy(values.selectedContentTypes).map(uid => {
187
+ const { attributes, info: { displayName } } = allContentTypes.find(item => item.uid == uid);
188
+ const stringAttributes = Object.keys(attributes).filter(_ => attributes[_].type === 'string');
189
+ const relationAttributes = Object.keys(attributes).filter(_ => attributes[_].type === 'relation');
190
+ const key = `collectionSettings-${uid}`;
191
+ return (<Accordion
192
+ expanded={contentTypeExpanded === key}
193
+ toggle={() => handleSetContentTypeExpanded(key)}
194
+ key={key}
195
+ id={key}
196
+ size="S">
197
+ <AccordionToggle title={displayName} togglePosition="left" />
198
+ <AccordionContent>
199
+ <Box padding={6}>
200
+ <Stack size={4}>
201
+ <Select
202
+ name={`collectionSettings-${uid}-entryLabel`}
203
+ label={getMessage('pages.settings.form.nameField.label')}
204
+ hint={getMessage(`pages.settings.form.populate.${isEmpty(stringAttributes) ? 'empty' : 'hint'}`)}
205
+ placeholder={getMessage('pages.settings.form.nameField.placeholder')}
206
+ onClear={() => null}
207
+ value={values.nameFields[uid] || []}
208
+ onChange={(value) => setFieldValue('nameFields', prepareNameFieldFor(uid, values.nameFields, value))}
209
+ multi
210
+ withTags
211
+ disabled={isRestartRequired || isEmpty(stringAttributes)}
212
+ >
213
+ {stringAttributes.map(key =>
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>))}
230
+ </Select>
231
+ </Stack>
232
+ </Box>
233
+
234
+ </AccordionContent>
235
+ </Accordion>);
236
+ })}
237
+ </AccordionGroup>
238
+ </GridItem>)}
239
+ </Grid>
240
+ </Stack>
241
+ </Box>
242
+ <Box {...boxDefaultProps} >
243
+ <Stack size={4}>
244
+ <Typography variant="delta" as="h2">
245
+ {getMessage('pages.settings.additional.title')}
246
+ </Typography>
247
+ <Grid gap={4}>
175
248
  <GridItem col={3} s={6} xs={12}>
176
249
  <NumberInput
177
250
  name="allowedLevels"
@@ -183,20 +256,6 @@ const SettingsPage = () => {
183
256
  disabled={isRestartRequired}
184
257
  />
185
258
  </GridItem>
186
- </Grid>
187
- </Stack>
188
- </Box>
189
- <Box
190
- background="neutral0"
191
- hasRadius
192
- shadow="filterShadow"
193
- padding={6}
194
- >
195
- <Stack size={4}>
196
- <Typography variant="delta" as="h2">
197
- {getMessage('pages.settings.additional.title')}
198
- </Typography>
199
- <Grid gap={4}>
200
259
  <GridItem col={6} s={12} xs={12}>
201
260
  <ToggleInput
202
261
  name="audienceFieldChecked"
@@ -212,62 +271,7 @@ const SettingsPage = () => {
212
271
  </Grid>
213
272
  </Stack>
214
273
  </Box>
215
- {!isEmpty(values.selectedContentTypes) && (
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
- >
274
+ <Box {...boxDefaultProps} >
271
275
  <Stack size={4}>
272
276
  <Typography variant="delta" as="h2">
273
277
  {getMessage('pages.settings.restoring.title')}
@@ -308,4 +312,4 @@ const SettingsPage = () => {
308
312
  }
309
313
 
310
314
 
311
- export default SettingsPage;
315
+ export default SettingsPage;
@@ -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,18 +74,19 @@ const NavigationItemForm = ({
76
74
  };
77
75
 
78
76
  const sanitizePayload = (payload = {}) => {
79
- const { onItemClick, onItemLevelAddClick, related, relatedType, menuAttached, ...purePayload } = payload;
80
- const sanitizedType = purePayload.type || navigationItemType.INTERNAL;
77
+ const { onItemClick, onItemLevelAddClick, related, relatedType, menuAttached, type, ...purePayload } = payload;
81
78
  const relatedId = related
82
79
  const relatedCollectionType = relatedType;
80
+ const title = payload.title || relatedSelectOptions.find(v => v.key == relatedId)?.label
83
81
  return {
84
82
  ...purePayload,
83
+ title,
84
+ type,
85
85
  menuAttached: isNil(menuAttached) ? false : menuAttached,
86
- type: sanitizedType,
87
- path: sanitizedType === navigationItemType.INTERNAL ? purePayload.path : undefined,
88
- externalPath: sanitizedType === navigationItemType.EXTERNAL ? purePayload.externalPath : undefined,
89
- related: relatedId,
90
- relatedType: relatedCollectionType,
86
+ path: type !== navigationItemType.EXTERNAL ? purePayload.path : undefined,
87
+ externalPath: type === navigationItemType.EXTERNAL ? purePayload.externalPath : undefined,
88
+ related: type === navigationItemType.INTERNAL ? relatedId : undefined,
89
+ relatedType: type === navigationItemType.INTERNAL ? relatedCollectionType : undefined,
91
90
  isSingle: isSingleSelected,
92
91
  uiRouterKey: generateUiRouterKey(purePayload.title, relatedId, relatedCollectionType),
93
92
  };
@@ -107,11 +106,8 @@ const NavigationItemForm = ({
107
106
  }
108
107
  };
109
108
 
110
- const onTypeChange = ({ target: { name, value } }) =>
111
- onChange({ target: { name, value: value ? navigationItemType.INTERNAL : navigationItemType.EXTERNAL } });
112
-
113
109
  const onAudienceChange = (value) => {
114
- onChange({target: {name: `${inputsPrefix}audience`, value}});
110
+ onChange({ target: { name: `${inputsPrefix}audience`, value } });
115
111
  }
116
112
 
117
113
  const onChange = ({ target: { name, value } }) => {
@@ -146,6 +142,20 @@ const NavigationItemForm = ({
146
142
  [relatedTypeSelectValue, contentTypes],
147
143
  );
148
144
 
145
+ const navigationItemTypeOptions = Object.keys(navigationItemType).map(key => {
146
+ const value = navigationItemType[key].toLowerCase();
147
+ return {
148
+ key,
149
+ value: navigationItemType[key],
150
+ metadatas: {
151
+ intlLabel: {
152
+ id: getTradId(`popup.item.form.type.${value}.label`),
153
+ defaultMessage: getTradId(`popup.item.form.type.${value}.label`),
154
+ }
155
+ }
156
+ }
157
+ });
158
+
149
159
  const relatedSelectOptions = contentTypeEntities
150
160
  .filter((item) => {
151
161
  const usedContentTypeEntitiesOfSameType = usedContentTypeEntities
@@ -176,7 +186,9 @@ const NavigationItemForm = ({
176
186
  const isExternal = form.type === navigationItemType.EXTERNAL;
177
187
  const pathSourceName = isExternal ? 'externalPath' : 'path';
178
188
 
179
- const submitDisabled = (form.type !== navigationItemType.EXTERNAL) && isNil(form.related);
189
+ const submitDisabled =
190
+ (form.type === navigationItemType.INTERNAL && isNil(get(form, `${inputsPrefix}related`))) ||
191
+ (form.type === navigationItemType.WRAPPER && isNil(get(form, `${inputsPrefix}title`)));
180
192
 
181
193
  const debouncedSearch = useCallback(
182
194
  debounce(nextValue => setContentTypeSearchQuery(nextValue), 500),
@@ -283,7 +295,21 @@ const NavigationItemForm = ({
283
295
  value={get(form, `${inputsPrefix}title`, '')}
284
296
  />
285
297
  </GridItem>
286
- <GridItem key={`${inputsPrefix}menuAttached`} col={6} lg={12}>
298
+ <GridItem key={`${inputsPrefix}type`} col={4} lg={12}>
299
+ <GenericInput
300
+ intlLabel={{
301
+ id: getTradId('popup.item.form.type.label'),
302
+ defaultMessage: 'Internal link',
303
+ }}
304
+ name={`${inputsPrefix}type`}
305
+ options={navigationItemTypeOptions}
306
+ type='select'
307
+ error={get(formErrors, `${inputsPrefix}type.id`)}
308
+ onChange={onChange}
309
+ value={get(form, `${inputsPrefix}type`, '')}
310
+ />
311
+ </GridItem>
312
+ <GridItem key={`${inputsPrefix}menuAttached`} col={4} lg={12}>
287
313
  <GenericInput
288
314
  intlLabel={{
289
315
  id: getTradId('popup.item.form.menuAttached.label'),
@@ -297,19 +323,6 @@ const NavigationItemForm = ({
297
323
  disabled={!(data.isMenuAllowedLevel && data.parentAttachedToMenu)}
298
324
  />
299
325
  </GridItem>
300
- <GridItem key={`${inputsPrefix}type`} col={6} lg={12}>
301
- <GenericInput
302
- intlLabel={{
303
- id: getTradId('popup.item.form.type.label'),
304
- defaultMessage: 'Internal link',
305
- }}
306
- name={`${inputsPrefix}type`}
307
- type='bool'
308
- error={get(formErrors, `${inputsPrefix}type.id`)}
309
- onChange={onTypeChange}
310
- value={get(form, `${inputsPrefix}type`, '') === navigationItemType.INTERNAL}
311
- />
312
- </GridItem>
313
326
  <GridItem key={`${inputsPrefix}path`} col={12}>
314
327
  <GenericInput
315
328
  intlLabel={{
@@ -328,7 +341,7 @@ const NavigationItemForm = ({
328
341
  description={generatePreviewPath()}
329
342
  />
330
343
  </GridItem>
331
- {!isExternal && (
344
+ {get(form, `${inputsPrefix}type`) === navigationItemType.INTERNAL && (
332
345
  <>
333
346
  <GridItem col={6} lg={12}>
334
347
  <GenericInput
@@ -400,8 +413,14 @@ const NavigationItemForm = ({
400
413
  label={getMessage('popup.item.form.audience.label')}
401
414
  onChange={onAudienceChange}
402
415
  value={audience}
416
+ hint={
417
+ !isLoading && isEmpty(audienceOptions)
418
+ ? getMessage('popup.item.form.audience.empty', 'There are no more audiences')
419
+ : undefined
420
+ }
403
421
  multi
404
422
  withTags
423
+ disabled={isEmpty(audienceOptions)}
405
424
  >
406
425
  {audienceOptions.map(({ value, label }) => <Option key={value} value={value}>{label}</Option>)}
407
426
  </Select>
@@ -1,16 +1,16 @@
1
1
 
2
2
 
3
3
  import React from 'react';
4
- import { ButtonText } from '@strapi/design-system/Text';
4
+ import { Typography } from '@strapi/design-system/Typography';
5
5
  import { ModalHeader } from '@strapi/design-system/ModalLayout';
6
6
  import { getMessage } from '../../../../utils';
7
7
 
8
8
  export const NavigationItemPopupHeader = ({isNewItem}) => {
9
9
  return (
10
10
  <ModalHeader>
11
- <ButtonText textColor="neutral800" as="h2" id="asset-dialog-title">
11
+ <Typography variant="omega" fontWeight="bold" textColor="neutral800" as="h2" id="asset-dialog-title">
12
12
  {getMessage(`popup.item.header.${isNewItem ? 'new' : 'edit'}`)}
13
- </ButtonText>
13
+ </Typography>
14
14
  </ModalHeader>
15
15
  );
16
16
  };
@@ -120,6 +120,44 @@ 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
+
154
+ const handleItemReOrder = (item, newOrder) => {
155
+ handleSubmitNavigationItem({
156
+ ...item,
157
+ order: newOrder,
158
+ })
159
+ }
160
+
123
161
  const handleItemRemove = (item) => {
124
162
  handleSubmitNavigationItem({
125
163
  ...item,
@@ -134,6 +172,14 @@ const View = () => {
134
172
  });
135
173
  };
136
174
 
175
+ const handleItemToggleCollapse = (item) => {
176
+ handleSubmitNavigationItem({
177
+ ...item,
178
+ collapsed: !item.collapsed,
179
+ updated: true,
180
+ });
181
+ }
182
+
137
183
  const handleItemEdit = (
138
184
  item,
139
185
  levelPath = '',
@@ -157,6 +203,33 @@ const View = () => {
157
203
  setSearchValue('');
158
204
  }
159
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
+
160
233
  return (
161
234
  <Main labelledBy="title" aria-busy={isLoadingForSubmit}>
162
235
  <NavigationHeader
@@ -173,18 +246,15 @@ const View = () => {
173
246
  <>
174
247
  <NavigationContentHeader
175
248
  startActions={<Search value={searchValue} setValue={setSearchValue} />}
176
- endActions={<Button
177
- onClick={addNewNavigationItem}
178
- startIcon={<PlusIcon />}
179
- disabled={isLoadingForSubmit}
180
- type="submit"
181
- >
182
- {formatMessage(getTrad('header.action.newItem'))}
183
- </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
+ )}
184
254
  />
185
255
  {isEmpty(changedActiveNavigation.items || []) && (
186
256
  <Flex direction="column" minHeight="400px" justifyContent="center">
187
- <Icon as={EmptyDocumentsIcon} width="160px" height="88px" color=""/>
257
+ <Icon as={EmptyDocumentsIcon} width="160px" height="88px" color="" />
188
258
  <Box padding={4}>
189
259
  <Typography variant="beta" textColor="neutral600">{formatMessage(getTrad('empty'))}</Typography>
190
260
  </Box>
@@ -206,6 +276,8 @@ const View = () => {
206
276
  onItemRemove={handleItemRemove}
207
277
  onItemEdit={handleItemEdit}
208
278
  onItemRestore={handleItemRestore}
279
+ onItemReOrder={handleItemReOrder}
280
+ onItemToggleCollapse={handleItemToggleCollapse}
209
281
  displayFlat={!isSearchEmpty}
210
282
  root
211
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
  {
@@ -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 }) => {