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

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 (50) hide show
  1. package/README.md +55 -8
  2. package/__mocks__/pages.settings.json +25 -0
  3. package/__mocks__/strapi.js +207 -0
  4. package/admin/src/components/ConfirmationDialog/index.js +56 -0
  5. package/admin/src/components/Item/ItemCardBadge/index.js +2 -1
  6. package/admin/src/components/Item/ItemCardHeader/index.js +8 -13
  7. package/admin/src/components/Item/index.js +10 -13
  8. package/admin/src/components/RestartAlert/index.js +8 -0
  9. package/admin/src/components/Search/index.js +21 -23
  10. package/admin/src/hooks/useAllContentTypes.js +13 -0
  11. package/admin/src/hooks/useNavigationConfig.js +58 -0
  12. package/admin/src/index.js +24 -1
  13. package/admin/src/pages/SettingsPage/index.js +311 -0
  14. package/admin/src/pages/View/components/NavigationHeader/index.js +39 -23
  15. package/admin/src/pages/View/components/NavigationItemForm/index.js +49 -9
  16. package/admin/src/pages/View/components/NavigationItemPopup/NavigationItemPopupFooter.js +3 -6
  17. package/admin/src/pages/View/components/NavigationItemPopup/NavigationItemPopupHeader.js +3 -7
  18. package/admin/src/pages/View/components/NavigationItemPopup/index.js +3 -5
  19. package/admin/src/pages/View/index.js +29 -20
  20. package/admin/src/pages/View/utils/parsers.js +7 -3
  21. package/admin/src/translations/en.json +52 -10
  22. package/admin/src/translations/fr.json +4 -4
  23. package/admin/src/utils/api.js +51 -0
  24. package/admin/src/utils/index.js +20 -0
  25. package/package.json +7 -6
  26. package/server/bootstrap.js +30 -1
  27. package/server/content-types/navigation/schema.json +45 -0
  28. package/server/content-types/navigation-item/schema.json +1 -1
  29. package/server/controllers/navigation.js +30 -5
  30. package/server/graphql/index.js +3 -4
  31. package/server/graphql/queries/render-navigation.js +4 -3
  32. package/server/graphql/types/content-types-name-fields.js +4 -2
  33. package/server/graphql/types/navigation-related.js +2 -2
  34. package/server/routes/admin.js +24 -1
  35. package/server/services/__tests__/functions.test.js +48 -0
  36. package/server/services/__tests__/navigation.test.js +84 -77
  37. package/server/services/navigation.js +58 -18
  38. package/server/services/utils/functions.js +45 -12
  39. package/strapi-server.js +0 -2
  40. package/yarn-error.log +5263 -0
  41. package/.circleci/config.yml +0 -48
  42. package/.eslintrc +0 -35
  43. package/.github/pull_request_template.md +0 -13
  44. package/.github/stale.yml +0 -15
  45. package/.nvmrc +0 -1
  46. package/codecov.yml +0 -3
  47. package/public/assets/logo.png +0 -0
  48. package/public/assets/preview.png +0 -0
  49. package/server/content-types/navigation/schema.js +0 -45
  50. package/server/register.js +0 -5
@@ -16,6 +16,16 @@ module.exports = {
16
16
  path: '/config',
17
17
  handler: 'navigation.config',
18
18
  },
19
+ {
20
+ method: 'PUT',
21
+ path: '/config',
22
+ handler: 'navigation.updateConfig',
23
+ },
24
+ {
25
+ method: 'DELETE',
26
+ path: '/config',
27
+ handler: 'navigation.restoreConfig',
28
+ },
19
29
  {
20
30
  method: 'GET',
21
31
  path: '/:id',
@@ -33,6 +43,19 @@ module.exports = {
33
43
  policies: [
34
44
  'admin::isAuthenticatedAdmin'
35
45
  ]
36
- }
46
+ },
47
+ {
48
+ method: 'GET',
49
+ path: '/settings/config',
50
+ handler: 'navigation.settingsConfig',
51
+ },
52
+ {
53
+ method: 'GET',
54
+ path: '/settings/restart',
55
+ handler: 'navigation.settingsRestart',
56
+ config: {
57
+ policies: [],
58
+ },
59
+ },
37
60
  ]
38
61
  }
@@ -0,0 +1,48 @@
1
+ const { setupStrapi } = require('../../../__mocks__/strapi');
2
+ const utilsFunctionsFactory = require('../utils/functions');
3
+
4
+ describe('Utilities functions', () => {
5
+ beforeAll(async () => {
6
+ setupStrapi();
7
+ });
8
+
9
+ describe('Path rendering functions', () => {
10
+ it('Can build nested path structure', async () => {
11
+ const utilsFunctions = utilsFunctionsFactory({ strapi });
12
+ const { itemModel } = utilsFunctions.extractMeta(strapi.plugins);
13
+ const rootPath = '/home/side';
14
+ const entities = await strapi
15
+ .query(itemModel.uid)
16
+ .findMany({
17
+ where: {
18
+ master: 1
19
+ }
20
+ });
21
+ const nested = utilsFunctions.buildNestedPaths({ items: entities });
22
+
23
+ expect(nested.length).toBe(2);
24
+ expect(nested[1].path).toBe(rootPath);
25
+ });
26
+
27
+ it('Can filter items by path', async () => {
28
+ const utilsFunctions = utilsFunctionsFactory({ strapi });
29
+ const { itemModel } = utilsFunctions.extractMeta(strapi.plugins);
30
+ const rootPath = '/home/side';
31
+ const entities = await strapi
32
+ .query(itemModel.uid)
33
+ .findMany({
34
+ where: {
35
+ master: 1
36
+ }
37
+ });
38
+ const {
39
+ root,
40
+ items
41
+ } = utilsFunctions.filterByPath(entities, rootPath);
42
+
43
+ expect(root).toBeDefined();
44
+ expect(root.path).toBe(rootPath);
45
+ expect(items.length).toBe(1)
46
+ });
47
+ });
48
+ });
@@ -1,84 +1,91 @@
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);
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({ idOrSlug: 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({
38
+ idOrSlug: 1,
39
+ type: "TREE"
40
+ });
41
+
42
+ expect(result).toBeDefined()
43
+ expect(result.length).toBeGreaterThan(0)
44
+ });
45
+
46
+ it('Can render branch in rfr format', async () => {
47
+ const service = strapi.plugin('navigation').service('navigation');
48
+ const result = await service.render({
49
+ idOrSlug: 1,
50
+ type: "RFR"
81
51
  });
52
+
53
+ expect(result).toBeDefined()
54
+ expect(result.length).toBeGreaterThan(0)
55
+ });
56
+
57
+ it('Can render only menu attached elements', async () => {
58
+ const service = strapi.plugin('navigation').service('navigation');
59
+ const result = await service.render({
60
+ idOrSlug: 1,
61
+ type: "FLAT",
62
+ menuOnly: true.valueOf,
63
+ });
64
+
65
+ expect(result).toBeDefined()
66
+ expect(result.length).toBe(1)
67
+ });
68
+
69
+ it('Can render branch by path', async () => {
70
+ const service = strapi.plugin('navigation').service('navigation');
71
+ const result = await service.render({
72
+ idOrSlug: 1,
73
+ type: "FLAT",
74
+ rootPath: '/home/side'
75
+ });
76
+
77
+ expect(result).toBeDefined();
78
+ expect(result.length).toBe(1);
79
+ });
80
+ });
81
+
82
+ describe('Render child', () => {
83
+ it('Can render child', async () => {
84
+ const service = strapi.plugin('navigation').service('navigation');
85
+ const result = await service.renderChildren(1, "home");
86
+
87
+ expect(result).toBeDefined();
88
+ expect(result.length).toBe(1);
82
89
  });
83
90
  });
84
91
  });
@@ -19,7 +19,7 @@ const { KIND_TYPES } = require('./utils/constant');
19
19
  const utilsFunctionsFactory = require('./utils/functions');
20
20
  const { renderType } = require('../content-types/navigation/lifecycle');
21
21
  const { type: itemType, additionalFields: configAdditionalFields } = require('../content-types/navigation-item').lifecycle;
22
- const { NotFoundError } = require('@strapi/utils').errors
22
+ const { NotFoundError } = require('@strapi/utils').errors
23
23
  const excludedContentTypes = ['strapi::'];
24
24
  const contentTypesNameFieldsDefaults = ['title', 'subject', 'name'];
25
25
 
@@ -56,7 +56,7 @@ module.exports = ({ strapi }) => {
56
56
  limit: -1,
57
57
  },
58
58
  sort: ['order:asc'],
59
- populate: ['related', 'parent']
59
+ populate: ['related', 'parent', 'audience']
60
60
  });
61
61
  const entities = await this.getRelatedItems(entityItems);
62
62
  return {
@@ -65,27 +65,35 @@ module.exports = ({ strapi }) => {
65
65
  };
66
66
  },
67
67
 
68
+ async restart() {
69
+ setImmediate(() => strapi.reload());
70
+ },
71
+
68
72
  // Get plugin config
69
- async config() {
70
- const { pluginName, audienceModel } = utilsFunctions.extractMeta(strapi.plugins);
71
- const additionalFields = strapi.plugin(pluginName).config('additionalFields')
72
- const contentTypesNameFields = strapi.plugin(pluginName).config('contentTypesNameFields');
73
- const allowedLevels = strapi.plugin(pluginName).config('allowedLevels');
73
+ async config(viaSettingsPage = false) {
74
+ const { audienceModel, service } = utilsFunctions.extractMeta(strapi.plugins);
75
+ const pluginStore = await strapi.store({ type: 'plugin', name: 'navigation' });
76
+ const config = await pluginStore.get({ key: 'config' });
77
+ const additionalFields = config.additionalFields;
78
+ const contentTypesNameFields = config.contentTypesNameFields;
79
+ const allowedLevels = config.allowedLevels;
80
+ const isGQLPluginEnabled = !isNil(strapi.plugin('graphql'));
74
81
 
75
82
  let extendedResult = {};
76
83
  const result = {
77
- contentTypes: await strapi.plugin(pluginName).service('navigation').configContentTypes(),
84
+ contentTypes: await service.configContentTypes(),
78
85
  contentTypesNameFields: {
79
86
  default: contentTypesNameFieldsDefaults,
80
87
  ...(isObject(contentTypesNameFields) ? contentTypesNameFields : {}),
81
88
  },
82
89
  allowedLevels,
83
90
  additionalFields,
91
+ isGQLPluginEnabled: viaSettingsPage ? isGQLPluginEnabled : undefined,
84
92
  };
85
93
 
86
94
  if (additionalFields.includes(configAdditionalFields.AUDIENCE)) {
87
95
  const audienceItems = await strapi
88
- .query(`plugin::${pluginName}.${audienceModel.modelName}`)
96
+ .query(audienceModel.uid)
89
97
  .findMany({
90
98
  paggination: {
91
99
  limit: -1,
@@ -102,10 +110,33 @@ module.exports = ({ strapi }) => {
102
110
  };
103
111
  },
104
112
 
113
+ async updateConfig(newConfig) {
114
+ const pluginStore = await strapi.store({ type: 'plugin', name: 'navigation' });
115
+ await pluginStore.set({ key: 'config', value: newConfig });
116
+ },
117
+
118
+ 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
+ });
132
+ },
133
+
105
134
  async configContentTypes() {
135
+ const pluginStore = strapi.store({ type: 'plugin', name: 'navigation' });
136
+ const config = await pluginStore.get({ key: 'config' });
106
137
  const eligibleContentTypes =
107
138
  await Promise.all(
108
- strapi.plugin('navigation').config('contentTypes')
139
+ config.contentTypes
109
140
  .filter(contentType => !!strapi.contentTypes[contentType])
110
141
  .map(
111
142
  async (key) => {
@@ -311,20 +342,20 @@ module.exports = ({ strapi }) => {
311
342
  ...(type === renderType.FLAT ? { uiRouterKey: childUIKey } : {}),
312
343
  };
313
344
 
314
- return service.renderType(type, criteria, itemCriteria, filter);
345
+ return service.renderType({ type, criteria, itemCriteria, filter });
315
346
  },
316
347
 
317
- async render(idOrSlug, type = renderType.FLAT, menuOnly = false) {
348
+ async render({ idOrSlug, type = renderType.FLAT, menuOnly = false, rootPath = null }) {
318
349
  const { service } = utilsFunctions.extractMeta(strapi.plugins);
319
350
 
320
351
  const findById = !isNaN(toNumber(idOrSlug)) || isUuid(idOrSlug);
321
352
  const criteria = findById ? { id: idOrSlug } : { slug: idOrSlug };
322
353
  const itemCriteria = menuOnly ? { menuAttached: true } : {};
323
354
 
324
- return service.renderType(type, criteria, itemCriteria);
355
+ return service.renderType({ type, criteria, itemCriteria, rootPath });
325
356
  },
326
357
 
327
- async renderType(type = renderType.FLAT, criteria = {}, itemCriteria = {}, filter = null) {
358
+ async renderType({ type = renderType.FLAT, criteria = {}, itemCriteria = {}, filter = null, rootPath = null }) {
328
359
  const { pluginName, service, masterModel, itemModel } = utilsFunctions.extractMeta(
329
360
  strapi.plugins,
330
361
  );
@@ -362,8 +393,8 @@ module.exports = ({ strapi }) => {
362
393
  const getTemplateName = await utilsFunctions.templateNameFactory(items, strapi, contentTypes);
363
394
  const itemParser = (item, path = '', field) => {
364
395
  const isExternal = item.type === itemType.EXTERNAL;
365
- const parentPath = isExternal ? undefined : `${path === '/' ? '' : path}/${item.path === '/'
366
- ? ''
396
+ const parentPath = isExternal ? undefined : `${path === '/' ? '' : path}/${first(item.path) === '/'
397
+ ? item.path.substring(1)
367
398
  : item.path}`;
368
399
  const slug = isString(parentPath) ? slugify(
369
400
  (first(parentPath) === '/' ? parentPath.substring(1) : parentPath).replace(/\//g, '-')) : undefined;
@@ -391,9 +422,17 @@ module.exports = ({ strapi }) => {
391
422
  }),
392
423
  };
393
424
  };
425
+
426
+ const {
427
+ items: itemsFilteredByPath,
428
+ root: rootElement,
429
+ } = utilsFunctions.filterByPath(items, rootPath);
430
+
394
431
  const treeStructure = service.renderTree({
395
- items,
432
+ items: isNil(rootPath) ? items : itemsFilteredByPath,
396
433
  field: 'parent',
434
+ id: get(rootElement, 'parent.id'),
435
+ path: get(rootElement, 'parent.path'),
397
436
  itemParser,
398
437
  });
399
438
 
@@ -409,7 +448,7 @@ module.exports = ({ strapi }) => {
409
448
  }
410
449
  return filteredStructure;
411
450
  default:
412
- return items
451
+ const publishedItems = items
413
452
  .filter(utilsFunctions.filterOutUnpublished)
414
453
  .map((item) => ({
415
454
  ...item,
@@ -418,6 +457,7 @@ module.exports = ({ strapi }) => {
418
457
  related: item.related?.map(({ localizations, ...item }) => item),
419
458
  items: null,
420
459
  }));
460
+ return isNil(rootPath) ? items : utilsFunctions.filterByPath(publishedItems, rootPath).items;
421
461
  }
422
462
  }
423
463
  throw new NotFoundError();
@@ -6,6 +6,7 @@ const {
6
6
  find,
7
7
  isString,
8
8
  get,
9
+ isNil,
9
10
  } = require('lodash');
10
11
 
11
12
  const { type: itemType } = require('../../content-types/navigation-item/lifecycle');
@@ -20,19 +21,12 @@ module.exports = ({ strapi }) => {
20
21
 
21
22
  extractMeta(plugins) {
22
23
  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
24
  return {
31
- masterModel,
32
- itemModel,
33
- relatedModel,
34
- audienceModel,
35
- service,
25
+ masterModel: plugin.contentType('navigation'),
26
+ itemModel: plugin.contentType('navigation-item'),
27
+ relatedModel: plugin.contentType('navigations-items-related'),
28
+ audienceModel: plugin.contentType('audience'),
29
+ service: plugin.service('navigation'),
36
30
  plugin,
37
31
  pluginName: 'navigation',
38
32
  };
@@ -59,6 +53,45 @@ module.exports = ({ strapi }) => {
59
53
  });
60
54
  },
61
55
 
56
+ buildNestedPaths({items, id = null, field = 'parent', parentPath = null}){
57
+ return items
58
+ .filter(entity => {
59
+ if (entity[field] == null && id === null) {
60
+ return true;
61
+ }
62
+ let data = entity[field];
63
+ if (data && typeof id === 'string') {
64
+ data = data.toString();
65
+ }
66
+ return (data && data === id) || (isObject(entity[field]) && (entity[field].id === id));
67
+ })
68
+ .reduce((acc, entity) => {
69
+ const path = `${parentPath || ''}/${entity.path}`
70
+ return [
71
+ {
72
+ id: entity.id,
73
+ parent: parentPath && {
74
+ id: get(entity, 'parent.id'),
75
+ path: parentPath,
76
+ },
77
+ path
78
+ },
79
+ ...this.buildNestedPaths({items, id: entity.id, field, parentPath: path}),
80
+ ...acc,
81
+ ];
82
+ }, [])
83
+ },
84
+
85
+ filterByPath(items, path) {
86
+ const itemsWithPaths = this.buildNestedPaths({ items }).filter(({path: itemPath}) => itemPath.includes(path));
87
+ const root = itemsWithPaths.find(({ path: itemPath }) => itemPath === path);
88
+
89
+ return {
90
+ root,
91
+ items: isNil(root) ? [] : items.filter(({ id }) => (itemsWithPaths.find(v => v.id === id))),
92
+ }
93
+ },
94
+
62
95
  prepareAuditLog(actions) {
63
96
  return [
64
97
  ...new Set(
package/strapi-server.js CHANGED
@@ -4,7 +4,6 @@ const routes = require('./server/routes');
4
4
  const controllers = require('./server/controllers');
5
5
  const contentTypes = require('./server/content-types');
6
6
  const config = require('./server/config');
7
- const register = require('./server/register');
8
7
 
9
8
 
10
9
  module.exports = () => {
@@ -15,6 +14,5 @@ module.exports = () => {
15
14
  controllers,
16
15
  services,
17
16
  contentTypes,
18
- register,
19
17
  };
20
18
  };