strapi-plugin-navigation 2.0.0-rc.1 → 2.0.0

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
@@ -1,7 +1,9 @@
1
- <div align="center">
1
+ <div align="center" width="150px">
2
2
  <img style="width: 150px; height: auto;" src="public/assets/logo.png" alt="Logo - Strapi Navigation plugin" />
3
+ </div>
4
+ <div align="center">
3
5
  <h1>Strapi v4 - Navigation plugin</h1>
4
- <p>Create consumable navigation with a simple and straighthforward visual builder</p>
6
+ <p>Create consumable navigation with a simple and straightforward visual builder</p>
5
7
  <a href="https://www.npmjs.org/package/strapi-plugin-navigation">
6
8
  <img alt="GitHub package.json version" src="https://img.shields.io/github/package-json/v/VirtusLab-Open-Source/strapi-plugin-navigation?label=npm&logo=npm">
7
9
  </a>
@@ -73,7 +75,7 @@ Complete installation requirements are exact same as for Strapi itself and can b
73
75
 
74
76
  **Supported Strapi versions**:
75
77
 
76
- - Strapi v4.0.5 (recently tested)
78
+ - Strapi v4.0.7 (recently tested)
77
79
  - Strapi v4.x
78
80
 
79
81
  > 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).
@@ -393,7 +395,7 @@ Feel free to fork and make a Pull Request to this plugin project. All the input
393
395
 
394
396
  For general help using Strapi, please refer to [the official Strapi documentation](https://strapi.io/documentation/). For additional help, you can use one of these channels to ask a question:
395
397
 
396
- - [Slack](http://slack.strapi.io) We're present on official Strapi slack workspace. Look for @cyp3r and DM.
398
+ - [Discord](https://discord.strapi.io/) We're present on official Strapi Discord workspace. Find us by `[VirtusLab]` prefix and DM.
397
399
  - [Slack - VirtusLab Open Source](https://virtuslab-oss.slack.com) We're present on a public channel #strapi-molecules
398
400
  - [GitHub](https://github.com/VirtusLab/strapi-plugin-navigation/issues) (Bug reports, Contributions, Questions and Discussions)
399
401
  - [E-mail](mailto:strapi@virtuslab.com) - we will respond back as soon as possible
@@ -0,0 +1,25 @@
1
+ {
2
+ "kind": "collectionType",
3
+ "collectionName": "pages",
4
+ "info": {
5
+ "singularName": "page",
6
+ "pluralName": "pages",
7
+ "displayName": "Page",
8
+ "name": "page"
9
+ },
10
+ "options": {
11
+ "increments": true,
12
+ "timestamps": true,
13
+ "searchable": true,
14
+ "previewable": true,
15
+ "draftAndPublish": false
16
+ },
17
+ "pluginOptions": {},
18
+ "attributes": {
19
+ "title": {
20
+ "type": "string",
21
+ "required": true
22
+ }
23
+ }
24
+ }
25
+
@@ -0,0 +1,196 @@
1
+ const { isMatch } = require('lodash');
2
+
3
+ const masterModelMock = {
4
+ findOne: () => ({
5
+ id: 1,
6
+ name: "Main navigation",
7
+ slug: "main-navigation",
8
+ visible: true,
9
+ createdAt: "2021-12-30T14:05:50.276Z",
10
+ updatedAt: "2021-12-30T14:05:50.276Z",
11
+ }),
12
+ findMany: () => [{
13
+ id: 1,
14
+ name: "Main navigation",
15
+ slug: "main-navigation",
16
+ visible: true,
17
+ createdAt: "2021-12-30T14:05:50.276Z",
18
+ updatedAt: "2021-12-30T14:05:50.276Z",
19
+ }],
20
+ };
21
+
22
+ const itemModelMock = {
23
+ findOne: async () => ({
24
+ id: 1,
25
+ title: "home",
26
+ type: "INTERNAL",
27
+ path: "home1",
28
+ externalPath: null,
29
+ uiRouterKey: "home",
30
+ menuAttached: true,
31
+ order: 1,
32
+ createdAt: "2021-12-31T10:04:54.812Z",
33
+ updatedAt: "2022-01-14T13:36:29.430Z",
34
+ related: {
35
+ id: 1,
36
+ related_id: "1",
37
+ related_type: "api::pages.pages",
38
+ field: "navigation",
39
+ order: 1,
40
+ master: "3",
41
+ createdAt: "2021-12-31T10:04:54.800Z",
42
+ updatedAt: "2021-12-31T10:04:54.800Z",
43
+ navigationItemId: 56,
44
+ },
45
+ parent: null,
46
+ }),
47
+ findMany: async ({where}) => [{
48
+ id: 1,
49
+ title: "home",
50
+ type: "INTERNAL",
51
+ path: "home1",
52
+ externalPath: null,
53
+ uiRouterKey: "home",
54
+ menuAttached: true,
55
+ order: 1,
56
+ createdAt: "2021-12-31T10:04:54.812Z",
57
+ updatedAt: "2022-01-14T13:36:29.430Z",
58
+ master: 1,
59
+ related: {
60
+ id: 1,
61
+ related_id: "1",
62
+ related_type: "api::pages.pages",
63
+ field: "navigation",
64
+ order: 1,
65
+ master: "3",
66
+ createdAt: "2021-12-31T10:04:54.800Z",
67
+ updatedAt: "2021-12-31T10:04:54.800Z",
68
+ navigationItemId: 56,
69
+ },
70
+ parent: null,
71
+ }, {
72
+ id: 2,
73
+ title: "side",
74
+ type: "INTERNAL",
75
+ path: "side",
76
+ externalPath: null,
77
+ uiRouterKey: "side",
78
+ menuAttached: false,
79
+ order: 1,
80
+ createdAt: "2021-12-31T10:04:54.824Z",
81
+ updatedAt: "2021-12-31T12:47:20.508Z",
82
+ master: 1,
83
+ related: {
84
+ id: 2,
85
+ related_id: "2",
86
+ related_type: "api::pages.pages",
87
+ field: "navigation",
88
+ order: 1,
89
+ master: "3",
90
+ createdAt: "2021-12-31T10:04:54.823Z",
91
+ updatedAt: "2021-12-31T10:04:54.823Z",
92
+ navigationItemId: 57,
93
+ },
94
+ parent: {
95
+ id: 1,
96
+ title: "home",
97
+ type: "INTERNAL",
98
+ path: "home1",
99
+ externalPath: null,
100
+ uiRouterKey: "home",
101
+ menuAttached: true,
102
+ order: 1,
103
+ createdAt: "2021-12-31T10:04:54.812Z",
104
+ updatedAt: "2022-01-14T13:36:29.430Z",
105
+ },
106
+ }].filter(item => isMatch(item, where)),
107
+ };
108
+
109
+ const pageModelMock = {
110
+ findOne: async ({ where }) => ({
111
+ "id": 1,
112
+ "attributes": {
113
+ "title": "Page nr 1",
114
+ "createdAt": "2022-01-19T08:22:31.244Z",
115
+ "updatedAt": "2022-01-19T08:22:31.244Z",
116
+ "publishedAt": null
117
+ }
118
+ }),
119
+ findMany: async ({ where }) => [{
120
+ "id": 1,
121
+ "attributes": {
122
+ "title": "Page nr 1",
123
+ "createdAt": "2022-01-19T08:22:31.244Z",
124
+ "updatedAt": "2022-01-19T08:22:31.244Z",
125
+ "publishedAt": null
126
+ }
127
+ }, {
128
+ "id": 2,
129
+ "attributes": {
130
+ "title": "Page nr 2",
131
+ "createdAt": "2022-01-19T08:22:50.821Z",
132
+ "updatedAt": "2022-01-19T08:22:50.821Z",
133
+ "publishedAt": null
134
+ }
135
+ }]
136
+
137
+ };
138
+
139
+ const plugins = (strapi) => ({
140
+ navigation: {
141
+ get services() { return require('../server/services') },
142
+ service: (key) => (require('../server/services'))[key]({ strapi }),
143
+ get contentTypes() { return require('../server/content-types') },
144
+ contentType: (key) => preparePluginContentType(require('../server/content-types')[key].schema, 'navigation'),
145
+ config: (key) => ({
146
+ ...require('../server/config').default,
147
+ contentTypes: ['api::pages.pages'],
148
+ })[key],
149
+ }
150
+ });
151
+
152
+ const contentTypes = {
153
+ 'api::pages.pages': {
154
+ ...require('./pages.settings.json'),
155
+ uid: 'api::pages.pages',
156
+ modelName: 'page',
157
+ },
158
+ };
159
+
160
+ const preparePluginContentType = (schema, plugin) => {
161
+ const { name } = schema.info;
162
+
163
+ return {
164
+ ...schema,
165
+ uid: `plugin::${plugin}.${name}`,
166
+ modelName: name,
167
+ }
168
+ }
169
+
170
+ const strapiFactory = (plugins, contentTypes) => ({
171
+ get plugins() { return plugins(strapi) },
172
+ plugin: (name) => plugins(strapi)[name],
173
+ get contentTypes() { return contentTypes },
174
+ contentType: (key) => contentTypes[key],
175
+ query: (model) => {
176
+ switch (model) {
177
+ case 'plugin::navigation.navigation':
178
+ return masterModelMock;
179
+ case 'plugin::navigation.navigation-item':
180
+ return itemModelMock;
181
+ case 'api::pages.pages':
182
+ return pageModelMock;
183
+ default:
184
+ return {
185
+ findOne: () => ({}),
186
+ findMany: () => [],
187
+ }
188
+ }
189
+ }
190
+ });
191
+
192
+ const setupStrapi = () => {
193
+ Object.defineProperty(global, 'strapi', { value: strapiFactory(plugins, contentTypes) });
194
+ }
195
+
196
+ module.exports = { setupStrapi };
@@ -6,6 +6,7 @@ const ItemCardBadge = styled(Badge)`
6
6
 
7
7
  ${ props => props.small && `
8
8
  padding: 0 4px;
9
+ margin: 0px ${props.theme.spaces[3]};
9
10
  vertical-align: middle;
10
11
 
11
12
  cursor: default;
@@ -4,6 +4,7 @@ import { useIntl } from 'react-intl';
4
4
  import { Flex } from '@strapi/design-system/Flex';
5
5
  import { IconButton } from '@strapi/design-system/IconButton';
6
6
  import { Typography } from '@strapi/design-system/Typography';
7
+ import { Icon } from '@strapi/design-system/Icon';
7
8
  import PencilIcon from '@strapi/icons/Pencil';
8
9
  import TrashIcon from '@strapi/icons/Trash';
9
10
  import RefreshIcon from '@strapi/icons/Refresh';
@@ -19,7 +20,7 @@ const ItemCardHeader = ({ title, path, icon, removed, onItemRemove, onItemEdit,
19
20
  return (
20
21
  <Wrapper>
21
22
  <Flex alignItems="center">
22
- {icon}
23
+ <Icon as={icon} />
23
24
  <Typography variant="omega" fontWeight="bold">
24
25
  {title}
25
26
  </Typography>
@@ -71,7 +71,7 @@ const Item = (props) => {
71
71
  <ItemCardHeader
72
72
  title={title}
73
73
  path={isExternal ? externalPath : absolutePath}
74
- icon={isExternal ? <Earth /> : <LinkIcon />}
74
+ icon={isExternal ? Earth : LinkIcon }
75
75
  onItemRemove={() => onItemRemove({
76
76
  ...item,
77
77
  relatedRef,
@@ -102,7 +102,6 @@ const Item = (props) => {
102
102
  </TextButton>
103
103
  { relatedItemLabel && (<Box>
104
104
  <ItemCardBadge
105
- style={{ marginRight: 4 }}
106
105
  borderColor={`${relatedBadgeColor}200`}
107
106
  backgroundColor={`${relatedBadgeColor}100`}
108
107
  textColor={`${relatedBadgeColor}600`}
@@ -131,7 +130,7 @@ const Item = (props) => {
131
130
  onItemRestore={onItemRestore}
132
131
  error={error}
133
132
  allowedLevels={allowedLevels}
134
- isParentAttachedToMenu={true}
133
+ isParentAttachedToMenu={menuAttached}
135
134
  items={item.items}
136
135
  level={level + 1}
137
136
  levelPath={absolutePath}
@@ -1,7 +1,7 @@
1
1
  import React, { useRef, useState, useEffect } from 'react';
2
2
  import { useIntl } from 'react-intl';
3
3
  import { IconButton } from '@strapi/design-system/IconButton';
4
- import { Searchbar, SearchForm } from '@strapi/design-system/Searchbar';
4
+ import { Searchbar } from '@strapi/design-system/Searchbar';
5
5
  import SearchIcon from "@strapi/icons/Search";
6
6
  import { getTradId } from '../../translations';
7
7
 
@@ -11,32 +11,30 @@ const Search = ({ value, setValue }) => {
11
11
  const { formatMessage } = useIntl();
12
12
 
13
13
  useEffect(() => {
14
- if (isOpen) {
15
- setTimeout(() => {
16
- wrapperRef.current.querySelector('input').focus();
17
- }, 0);
18
- }
14
+ if (isOpen) {
15
+ setTimeout(() => {
16
+ wrapperRef.current.querySelector('input').focus();
17
+ }, 0);
18
+ }
19
19
  }, [isOpen]);
20
-
20
+
21
21
  if (isOpen) {
22
22
  return (
23
23
  <div ref={wrapperRef}>
24
- <SearchForm>
25
- <Searchbar
26
- name="searchbar"
27
- onClear={() => setValue('')}
28
- value={value}
29
- size="S"
30
- onChange={(e) => setValue(e.target.value)}
31
- clearLabel="Clearing the search"
32
- placeholder={formatMessage({
33
- id: getTradId('popup.item.form.audience.placeholder'),
34
- defaultMessage: 'Type to start searching...',
35
- })}
36
- >
37
- Search for navigation items
38
- </Searchbar>
39
- </SearchForm>
24
+ <Searchbar
25
+ name="searchbar"
26
+ onClear={() => { setValue(''); setIsOpen(false); }}
27
+ value={value}
28
+ size="S"
29
+ onChange={(e) => setValue(e.target.value)}
30
+ clearLabel="Clearing the search"
31
+ placeholder={formatMessage({
32
+ id: getTradId('popup.item.form.audience.placeholder'),
33
+ defaultMessage: 'Type to start searching...',
34
+ })}
35
+ >
36
+ Search for navigation items
37
+ </Searchbar>
40
38
  </div>
41
39
  );
42
40
  } else {
@@ -7,43 +7,59 @@ import Check from '@strapi/icons/Check';
7
7
  import More from '@strapi/icons/More';
8
8
  import { getTrad } from '../../../../translations';
9
9
  import { MoreButton } from './styles';
10
-
10
+ import { Select, Option } from '@strapi/design-system/Select';
11
+ import { Box } from '@strapi/design-system/Box'
11
12
 
12
13
  const NavigationHeader = ({
14
+ activeNavigation,
15
+ availableNavigations,
13
16
  structureHasErrors,
14
- structureHAsChanged,
17
+ structureHasChanged,
18
+ handleChangeSelection,
15
19
  handleSave,
16
20
  }) => {
17
21
  const { formatMessage } = useIntl();
18
-
19
22
  return (
20
23
  <HeaderLayout
21
- primaryAction={
24
+ primaryAction={
22
25
  <Stack horizontal size={2}>
23
- <Button
24
- onClick={handleSave}
25
- startIcon={<Check />}
26
- disabled={structureHasErrors || !structureHAsChanged}
27
- type="submit"
26
+ <Box width="10vw">
27
+ <Select
28
+ type="select"
29
+ placeholder={'Change navigation'}
30
+ name={`navigationSelect`}
31
+ onChange={handleChangeSelection}
32
+ value={activeNavigation?.id}
33
+ size="S"
34
+ style={null}
28
35
  >
29
- {formatMessage(getTrad('submit.cta.save'))}
30
- </Button>
31
- {/* <MoreButton
36
+ {availableNavigations.map(({ id, name }) => <Option key={id} value={id}>{name}</Option>)}
37
+ </Select >
38
+ </Box>
39
+ <Button
40
+ onClick={handleSave}
41
+ startIcon={<Check />}
42
+ disabled={structureHasErrors || !structureHasChanged}
43
+ type="submit"
44
+ >
45
+ {formatMessage(getTrad('submit.cta.save'))}
46
+ </Button>
47
+ {/* <MoreButton
32
48
  id="more"
33
49
  label="More"
34
50
  icon={<More />}
35
51
  /> */}
36
- </Stack>
37
- }
38
- title={formatMessage({
39
- id: getTrad('header.title'),
40
- defaultMessage: 'UI Navigation',
41
- })}
42
- subtitle={formatMessage({
43
- id: getTrad('header.description'),
44
- defaultMessage: 'Define your portal navigation',
45
- })}
46
- />
52
+ </Stack>
53
+ }
54
+ title={formatMessage({
55
+ id: getTrad('header.title'),
56
+ defaultMessage: 'UI Navigation',
57
+ })}
58
+ subtitle={formatMessage({
59
+ id: getTrad('header.description'),
60
+ defaultMessage: 'Define your portal navigation',
61
+ })}
62
+ />
47
63
  );
48
64
  };
49
65
 
@@ -259,6 +259,10 @@ const NavigationItemForm = ({
259
259
  }}
260
260
  name={`${inputsPrefix}title`}
261
261
  placeholder={{
262
+ id: "e.g. Blog",
263
+ defaultMessage: 'e.g. Blog',
264
+ }}
265
+ description={{
262
266
  id: getTradId('popup.item.form.title.placeholder'),
263
267
  defaultMessage: 'e.g. Blog',
264
268
  }}
@@ -331,6 +335,15 @@ const NavigationItemForm = ({
331
335
  onChange={onChangeRelatedType}
332
336
  options={relatedTypeSelectOptions}
333
337
  value={relatedTypeSelectValue}
338
+ disabled={isLoading || isEmpty(relatedTypeSelectOptions)}
339
+ description={
340
+ !isLoading && isEmpty(relatedTypeSelectOptions)
341
+ ? {
342
+ id: getTradId('popup.item.form.relatedType.empty'),
343
+ defaultMessage: 'There are no more content types',
344
+ }
345
+ : undefined
346
+ }
334
347
  />
335
348
  </GridItem>
336
349
  {relatedTypeSelectValue && !isSingleSelected && (
@@ -6,14 +6,12 @@ import { ModalHeader } from '@strapi/design-system/ModalLayout';
6
6
  import { useIntl } from 'react-intl';
7
7
  import { getTrad } from '../../../../translations';
8
8
 
9
- export const NavigationItemPopupHeader = () => {
9
+ export const NavigationItemPopupHeader = ({isNewItem}) => {
10
10
  const { formatMessage } = useIntl();
11
11
  return (
12
12
  <ModalHeader>
13
13
  <ButtonText textColor="neutral800" as="h2" id="asset-dialog-title">
14
- {formatMessage(
15
- getTrad('popup.item.header'),
16
- )}
14
+ {formatMessage(getTrad(`popup.item.header.${isNewItem ? 'new' : 'edit'}`))}
17
15
  </ButtonText>
18
16
  </ModalHeader>
19
17
  );
@@ -80,7 +80,7 @@ const NavigationItemPopUp = ({
80
80
 
81
81
  return (
82
82
  <ModalLayout labelledBy="condition-modal-breadcrumbs" onClose={onClose} isOpen={isOpen}>
83
- <NavigationItemPopupHeader />
83
+ <NavigationItemPopupHeader isNewItem={!data.viewId}/>
84
84
  <NavigationItemForm
85
85
  data={prepareFormData(data)}
86
86
  isLoading={isLoading}
@@ -10,11 +10,13 @@ import { isEmpty, get } from "lodash";
10
10
 
11
11
  // Design System
12
12
  import { Main } from '@strapi/design-system/Main';
13
+ import { Flex } from '@strapi/design-system/Flex';
13
14
  import { ContentLayout } from '@strapi/design-system/Layout';
15
+ import { Typography } from '@strapi/design-system/Typography';
14
16
  import { Box } from '@strapi/design-system/Box';
17
+ import { Icon } from '@strapi/design-system/Icon';
15
18
  import { Button } from '@strapi/design-system/Button';
16
19
  import { LoadingIndicatorPage } from "@strapi/helper-plugin";
17
- import { EmptyStateLayout } from '@strapi/design-system/EmptyStateLayout';
18
20
  import EmptyDocumentsIcon from '@strapi/icons/EmptyDocuments';
19
21
  import PlusIcon from "@strapi/icons/Plus";
20
22
 
@@ -23,6 +25,7 @@ import List from '../../components/NavigationItemList';
23
25
  import NavigationContentHeader from './components/NavigationContentHeader';
24
26
  import NavigationHeader from './components/NavigationHeader';
25
27
  import NavigationItemPopUp from "./components/NavigationItemPopup";
28
+ import Search from '../../components/Search';
26
29
  import useDataManager from "../../hooks/useDataManager";
27
30
  import { getTrad } from '../../translations';
28
31
  import {
@@ -31,7 +34,6 @@ import {
31
34
  usedContentTypes,
32
35
  validateNavigationStructure,
33
36
  } from './utils/parsers';
34
- import Search from '../../components/Search';
35
37
 
36
38
  const View = () => {
37
39
  const {
@@ -150,11 +152,19 @@ const View = () => {
150
152
  changeNavigationItemPopupState(false);
151
153
  };
152
154
 
155
+ const handleChangeNavigationSelection = (...args) => {
156
+ handleChangeSelection(...args);
157
+ setSearchValue('');
158
+ }
159
+
153
160
  return (
154
161
  <Main labelledBy="title" aria-busy={isLoadingForSubmit}>
155
162
  <NavigationHeader
156
163
  structureHasErrors={structureHasErrors}
157
- structureHAsChanged={structureChanged}
164
+ structureHasChanged={structureChanged}
165
+ availableNavigations={availableNavigations}
166
+ activeNavigation={activeNavigation}
167
+ handleChangeSelection={handleChangeNavigationSelection}
158
168
  handleSave={handleSave}
159
169
  />
160
170
  <ContentLayout>
@@ -162,7 +172,7 @@ const View = () => {
162
172
  {changedActiveNavigation && (
163
173
  <>
164
174
  <NavigationContentHeader
165
- startActions={<Search value={searchValue} setValue={setSearchValue}/>}
175
+ startActions={<Search value={searchValue} setValue={setSearchValue} />}
166
176
  endActions={<Button
167
177
  onClick={addNewNavigationItem}
168
178
  startIcon={<PlusIcon />}
@@ -172,22 +182,21 @@ const View = () => {
172
182
  {formatMessage(getTrad('header.action.newItem'))}
173
183
  </Button>}
174
184
  />
175
- {isEmpty(changedActiveNavigation.items || []) && (<Box paddingTop={4} >
176
- <EmptyStateLayout
177
- action={
178
- <Button
179
- variant='secondary'
180
- startIcon={<PlusIcon />}
181
- label={formatMessage(getTrad('empty.cta'))}
182
- onClick={addNewNavigationItem}
183
- >
184
- {formatMessage(getTrad('empty.cta'))}
185
- </Button>
186
- }
187
- icon={<EmptyDocumentsIcon width='10rem' />}
188
- content={formatMessage(getTrad('empty'))}
189
- />
190
- </Box>
185
+ {isEmpty(changedActiveNavigation.items || []) && (
186
+ <Flex direction="column" minHeight="400px" justifyContent="center">
187
+ <Icon as={EmptyDocumentsIcon} width="160px" height="88px" color=""/>
188
+ <Box padding={4}>
189
+ <Typography variant="beta" textColor="neutral600">{formatMessage(getTrad('empty'))}</Typography>
190
+ </Box>
191
+ <Button
192
+ variant='secondary'
193
+ startIcon={<PlusIcon />}
194
+ label={formatMessage(getTrad('empty.cta'))}
195
+ onClick={addNewNavigationItem}
196
+ >
197
+ {formatMessage(getTrad('empty.cta'))}
198
+ </Button>
199
+ </Flex>
191
200
  )}
192
201
  {
193
202
  !isEmpty(changedActiveNavigation.items || [])
@@ -7,6 +7,7 @@ export const transformItemToRESTPayload = (
7
7
  parent = undefined,
8
8
  master = undefined,
9
9
  config = {},
10
+ parentAttachedToMenu = true,
10
11
  ) => {
11
12
  const {
12
13
  id,
@@ -34,7 +35,7 @@ export const transformItemToRESTPayload = (
34
35
  find(contentTypes,
35
36
  ct => ct.uid === relatedType) :
36
37
  undefined;
37
-
38
+ const itemAttachedToMenu = menuAttached && parentAttachedToMenu
38
39
  return {
39
40
  id,
40
41
  parent,
@@ -45,7 +46,7 @@ export const transformItemToRESTPayload = (
45
46
  removed,
46
47
  order,
47
48
  uiRouterKey,
48
- menuAttached,
49
+ menuAttached: itemAttachedToMenu,
49
50
  audience: audience.map((audienceItem) =>
50
51
  isObject(audienceItem) ? audienceItem.value : audienceItem,
51
52
  ),
@@ -60,7 +61,7 @@ export const transformItemToRESTPayload = (
60
61
  field: relatedContentType && relatedContentType.relatedField ? relatedContentType.relatedField : 'navigation',
61
62
  },
62
63
  ],
63
- items: items.map((iItem) => transformItemToRESTPayload(iItem, id, master, config)),
64
+ items: items.map((iItem) => transformItemToRESTPayload(iItem, id, master, config, itemAttachedToMenu)),
64
65
  };
65
66
  };
66
67
 
@@ -251,6 +252,9 @@ export const prepareItemToViewPayload = (items = [], viewParentId = null, config
251
252
  }));
252
253
 
253
254
  export const extractRelatedItemLabel = (item = {}, fields = {}, config = {}) => {
255
+ if (get(item, 'isSingle', false)) {
256
+ return get(item, 'labelSingular', '');
257
+ }
254
258
  const { contentTypes = [] } = config;
255
259
  const { __collectionUid } = item;
256
260
  const contentType = contentTypes.find(_ => _.uid === __collectionUid)
@@ -5,9 +5,10 @@
5
5
  "header.action.newItem": "New Item",
6
6
  "submit.cta.cancel": "Cancel",
7
7
  "submit.cta.save": "Save",
8
- "empty": "Navigation container is empty",
8
+ "empty": "Your navigation is empty",
9
9
  "empty.cta": "Create first item",
10
- "popup.item.header": "Manage navigation item",
10
+ "popup.item.header.edit": "Edit navigation item",
11
+ "popup.item.header.new": "New navigation item",
11
12
  "popup.item.form.title.label": "Title",
12
13
  "popup.item.form.title.placeholder": "Enter the item title or leave blank to pull from related entity",
13
14
  "popup.item.form.uiRouterKey.label": "UI router key",
@@ -28,6 +29,7 @@
28
29
  "popup.item.form.relatedSection.label": "Relation to",
29
30
  "popup.item.form.relatedType.label": "Content Type",
30
31
  "popup.item.form.relatedType.placeholder": "Select content type...",
32
+ "popup.item.form.relatedType.empty": "There are no content types to select",
31
33
  "popup.item.form.related.label": "Entity",
32
34
  "popup.item.form.related.empty": "There are no more entities of \"{ contentTypeName }\" to select",
33
35
  "popup.item.form.button.create": "Create item",
@@ -41,7 +43,7 @@
41
43
  "notification.navigation.item.relation": "Entity relation does not exist!",
42
44
  "notification.navigation.item.relation.status.draft": "draft",
43
45
  "notification.navigation.item.relation.status.published": "published",
44
- "navigation.item.action.newItem": "New nested item",
46
+ "navigation.item.action.newItem": "Add nested item",
45
47
  "navigation.item.badge.removed": "Removed",
46
48
  "navigation.item.badge.draft": "{type}: Draft",
47
49
  "navigation.item.badge.published": "{type}: Published"
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "strapi-plugin-navigation",
3
- "version": "2.0.0-rc.1",
3
+ "version": "2.0.0",
4
4
  "description": "Strapi - Navigation plugin",
5
5
  "strapi": {
6
6
  "name": "navigation",
7
- "icon": "hamburger",
8
- "description": "UI navigation management",
7
+ "displayName": "Navigation",
8
+ "description": "Create consumable navigation with a simple and straightforward visual builder",
9
9
  "kind": "plugin"
10
10
  },
11
11
  "repository": {
@@ -13,10 +13,11 @@
13
13
  "url": "https://github.com/VirtusLab/strapi-plugin-navigation"
14
14
  },
15
15
  "scripts": {
16
+ "publish": "npm publish --tag latest",
16
17
  "test:unit": "jest --verbose --coverage"
17
18
  },
18
19
  "dependencies": {
19
- "@strapi/utils": "^4.0.5",
20
+ "@strapi/utils": "^4.0.7",
20
21
  "uuid": "^8.3.0",
21
22
  "bad-words": "^3.0.3",
22
23
  "lodash": "^4.17.11",
@@ -0,0 +1,45 @@
1
+ {
2
+ "collectionName": "navigations",
3
+ "info": {
4
+ "singularName": "navigation",
5
+ "pluralName": "navigations",
6
+ "displayName": "Navigation",
7
+ "name": "navigation"
8
+ },
9
+ "options": {
10
+ "increments": true,
11
+ "comment": ""
12
+ },
13
+ "pluginOptions": {
14
+ "content-manager": {
15
+ "visible": true
16
+ },
17
+ "content-type-builder": {
18
+ "visible": false
19
+ }
20
+ },
21
+ "attributes": {
22
+ "name": {
23
+ "type": "text",
24
+ "configurable": false,
25
+ "required": true
26
+ },
27
+ "slug": {
28
+ "type": "uid",
29
+ "target": "name",
30
+ "configurable": false,
31
+ "required": true
32
+ },
33
+ "visible": {
34
+ "type": "boolean",
35
+ "default": false,
36
+ "configurable": false
37
+ },
38
+ "items": {
39
+ "type": "relation",
40
+ "relation": "oneToMany",
41
+ "target": "plugin::navigation.navigation-item",
42
+ "configurable": false
43
+ }
44
+ }
45
+ }
@@ -4,7 +4,7 @@
4
4
  "singularName": "navigation-item",
5
5
  "pluralName": "navigation-items",
6
6
  "displayName": "Navigation Item",
7
- "tableName": "navigation-item"
7
+ "name": "navigation-item"
8
8
  },
9
9
  "options": {
10
10
  "increments": true,
@@ -1,84 +1,69 @@
1
- const { setupStrapi } = require('../../__mocks__/helpers/strapi');
1
+ const { setupStrapi } = require('../../../__mocks__/strapi');
2
2
 
3
- beforeAll(setupStrapi);
3
+ describe('Navigation services', () => {
4
+ beforeAll(async () => {
5
+ setupStrapi();
6
+ });
7
+
8
+ describe('Correct config', () => {
9
+ it('Declares Strapi instance', () => {
10
+ expect(strapi).toBeDefined()
11
+ expect(strapi.plugin('navigation').service('navigation')).toBeDefined()
12
+ });
4
13
 
5
- describe('Navigation service', () => {
6
- it('Strapi is defined', () => {
7
- expect(strapi).toBeDefined();
8
- expect(strapi.contentTypes).toBeDefined();
9
- expect(Object.keys(strapi.contentTypes).length).toBe(6);
14
+ it('Defines proper content types', () => {
15
+ expect(strapi.contentTypes).toBeDefined()
16
+ expect(strapi.plugin('navigation').contentTypes).toBeDefined()
17
+ });
18
+
19
+ it('Can read and return plugins config', () => {
20
+ expect(strapi.plugin('navigation').config('contentTypes')).toBeDefined()
21
+ expect(strapi.plugin('navigation').config('allowedLevels')).toBeDefined()
22
+ expect(strapi.plugin('navigation').config()).not.toBeDefined()
23
+ });
10
24
  });
11
- it('Config Content Types', () => {
12
- const { configContentTypes } = require('../navigation');
13
- const results = [
14
- {
15
- uid: 'application::pages.pages',
16
- collectionName: 'pages',
17
- isSingle: false,
18
- contentTypeName: 'Pages',
19
- endpoint: 'pages',
20
- label: 'Pages',
21
- labelSingular: 'Page',
22
- name: 'page',
23
- visible: true,
24
- }, {
25
- uid: 'application::blog-post.blog-post',
26
- collectionName: 'blog_posts',
27
- isSingle: false,
28
- contentTypeName: 'BlogPost',
29
- endpoint: 'blog-posts',
30
- label: 'Blog posts',
31
- labelSingular: 'Blog post',
32
- name: 'blog-post',
33
- visible: true,
34
- }, {
35
- uid: 'application::my-homepages.my-homepage',
36
- collectionName: 'my-homepages',
37
- isSingle: true,
38
- contentTypeName: 'MyHomepage',
39
- endpoint: 'my-homepage',
40
- label: 'My Homepage',
41
- labelSingular: 'My Homepage',
42
- name: 'my-homepage',
43
- visible: true,
44
- }, {
45
- uid: 'application::page-homes.home-page',
46
- collectionName: 'page_homes',
47
- isSingle: true,
48
- contentTypeName: 'HomePage',
49
- endpoint: 'custom-api',
50
- label: 'Page Home',
51
- labelSingular: 'Page Home',
52
- name: 'home-page',
53
- visible: true,
54
- }, {
55
- uid: 'plugins::another-plugin.pages',
56
- collectionName: 'pages',
57
- isSingle: false,
58
- contentTypeName: 'Plugin-pages',
59
- endpoint: 'plugin-pages',
60
- label: 'Pages',
61
- labelSingular: 'Page',
62
- name: 'plugin-page',
63
- visible: true,
64
- plugin: 'another-plugin',
65
- }, {
66
- uid: 'plugins::another-plugin.blog-post',
67
- collectionName: 'blog_posts',
68
- isSingle: false,
69
- contentTypeName: 'BlogPost',
70
- endpoint: 'plugin-blog-posts',
71
- label: 'Blog posts',
72
- labelSingular: 'Blog post',
73
- name: 'plugin-blog-post',
74
- visible: true,
75
- plugin: 'another-plugin',
76
- }];
77
- return configContentTypes().then(types => {
78
- types.map(type => {
79
- const result = results.find(({ uid }) => uid === type.uid);
80
- expect(type).toMatchObject(result);
81
- });
25
+
26
+ describe('Render navigation', () => {
27
+ it('Can render branch in flat format', async () => {
28
+ const service = strapi.plugin('navigation').service('navigation');
29
+ const result = await service.render(1);
30
+
31
+ expect(result).toBeDefined()
32
+ expect(result.length).toBe(2)
33
+ });
34
+
35
+ it('Can render branch in tree format', async () => {
36
+ const service = strapi.plugin('navigation').service('navigation');
37
+ const result = await service.render(1, "TREE");
38
+
39
+ expect(result).toBeDefined()
40
+ expect(result.length).toBeGreaterThan(0)
41
+ });
42
+
43
+ it('Can render branch in rfr format', async () => {
44
+ const service = strapi.plugin('navigation').service('navigation');
45
+ const result = await service.render(1, "RFR");
46
+
47
+ expect(result).toBeDefined()
48
+ expect(result.length).toBeGreaterThan(0)
49
+ });
50
+
51
+ it('Can render only menu attached elements', async () => {
52
+ const service = strapi.plugin('navigation').service('navigation');
53
+ const result = await service.render(1, "FLAT", true);
54
+
55
+ expect(result).toBeDefined()
56
+ expect(result.length).toBe(1)
57
+ });
58
+ });
59
+
60
+ describe('Render child', () => {
61
+ it('Can render child', async () => {
62
+ const service = strapi.plugin('navigation').service('navigation');
63
+ const result = await service.renderChildren(1, "home");
64
+
65
+ expect(result).toBeDefined();
66
+ expect(result.length).toBe(1);
82
67
  });
83
68
  });
84
69
  });
@@ -67,14 +67,14 @@ module.exports = ({ strapi }) => {
67
67
 
68
68
  // Get plugin config
69
69
  async config() {
70
- const { pluginName, audienceModel } = utilsFunctions.extractMeta(strapi.plugins);
70
+ const { pluginName, audienceModel, service } = utilsFunctions.extractMeta(strapi.plugins);
71
71
  const additionalFields = strapi.plugin(pluginName).config('additionalFields')
72
72
  const contentTypesNameFields = strapi.plugin(pluginName).config('contentTypesNameFields');
73
73
  const allowedLevels = strapi.plugin(pluginName).config('allowedLevels');
74
74
 
75
75
  let extendedResult = {};
76
76
  const result = {
77
- contentTypes: await strapi.plugin(pluginName).service('navigation').configContentTypes(),
77
+ contentTypes: await service.configContentTypes(),
78
78
  contentTypesNameFields: {
79
79
  default: contentTypesNameFieldsDefaults,
80
80
  ...(isObject(contentTypesNameFields) ? contentTypesNameFields : {}),
@@ -85,7 +85,7 @@ module.exports = ({ strapi }) => {
85
85
 
86
86
  if (additionalFields.includes(configAdditionalFields.AUDIENCE)) {
87
87
  const audienceItems = await strapi
88
- .query(`plugin::${pluginName}.${audienceModel.modelName}`)
88
+ .query(audienceModel.uid)
89
89
  .findMany({
90
90
  paggination: {
91
91
  limit: -1,
@@ -20,19 +20,12 @@ module.exports = ({ strapi }) => {
20
20
 
21
21
  extractMeta(plugins) {
22
22
  const { navigation: plugin } = plugins;
23
- const { navigation: service } = plugin.services;
24
- const {
25
- navigation: masterModel,
26
- 'navigation-item': itemModel,
27
- audience: audienceModel,
28
- 'navigations-items-related': relatedModel,
29
- } = plugin.contentTypes;
30
23
  return {
31
- masterModel,
32
- itemModel,
33
- relatedModel,
34
- audienceModel,
35
- service,
24
+ masterModel: plugin.contentType('navigation'),
25
+ itemModel: plugin.contentType('navigation-item'),
26
+ relatedModel: plugin.contentType('navigations-items-related'),
27
+ audienceModel: plugin.contentType('audience'),
28
+ service: plugin.service('navigation'),
36
29
  plugin,
37
30
  pluginName: 'navigation',
38
31
  };
@@ -1,45 +0,0 @@
1
- module.exports = {
2
- collectionName: "navigations",
3
- info: {
4
- singularName: "navigation",
5
- pluralName: "navigations",
6
- displayName: "Navigation",
7
- name: "navigation"
8
- },
9
- options: {
10
- increments: true,
11
- comment: ""
12
- },
13
- pluginOptions: {
14
- "content-manager": {
15
- visible: true
16
- },
17
- "content-type-builder": {
18
- visible: false
19
- }
20
- },
21
- attributes: {
22
- name: {
23
- type: "text",
24
- configurable: false,
25
- required: true
26
- },
27
- slug: {
28
- type: "uid",
29
- target: "name",
30
- configurable: false,
31
- required: true
32
- },
33
- visible: {
34
- type: "boolean",
35
- default: false,
36
- configurable: false
37
- },
38
- items: {
39
- type: "relation",
40
- relation: "oneToMany",
41
- target: "plugin::navigation.navigation-item",
42
- configurable: false
43
- }
44
- }
45
- }