strapi-plugin-navigation 2.0.0-beta.1 → 2.0.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +62 -104
  2. package/admin/src/components/Item/ItemCardHeader/index.js +2 -2
  3. package/admin/src/components/Item/index.js +3 -1
  4. package/admin/src/components/NavigationItemList/index.js +2 -0
  5. package/admin/src/components/Search/index.js +49 -0
  6. package/admin/src/pages/DataManagerProvider/index.js +6 -9
  7. package/admin/src/pages/DataManagerProvider/reducer.js +58 -69
  8. package/admin/src/pages/View/index.js +16 -4
  9. package/admin/src/pages/View/utils/parsers.js +1 -1
  10. package/admin/src/pluginId.js +2 -2
  11. package/package.json +7 -3
  12. package/server/config/index.js +8 -0
  13. package/server/controllers/navigation.js +21 -0
  14. package/server/graphql/index.js +23 -0
  15. package/server/graphql/queries/index.js +17 -0
  16. package/server/graphql/queries/render-navigation-child.js +16 -0
  17. package/server/graphql/queries/render-navigation.js +15 -0
  18. package/server/graphql/resolvers-config.js +4 -0
  19. package/server/graphql/types/content-types-name-fields.js +8 -0
  20. package/server/graphql/types/content-types.js +16 -0
  21. package/server/graphql/types/create-navigation-item.js +17 -0
  22. package/server/graphql/types/create-navigation-related.js +8 -0
  23. package/server/graphql/types/create-navigation.js +7 -0
  24. package/server/graphql/types/index.js +15 -0
  25. package/server/graphql/types/navigation-config.js +9 -0
  26. package/server/graphql/types/navigation-details.js +10 -0
  27. package/server/graphql/types/navigation-item.js +29 -0
  28. package/server/graphql/types/navigation-related.js +23 -0
  29. package/server/graphql/types/navigation-render-type.js +4 -0
  30. package/server/graphql/types/navigation.js +9 -0
  31. package/server/register.js +5 -0
  32. package/server/routes/client.js +21 -0
  33. package/server/routes/index.js +2 -1
  34. package/server/services/navigation.js +272 -6
  35. package/server/services/utils/functions.js +84 -2
  36. package/strapi-server.js +3 -1
@@ -8,11 +8,18 @@ const {
8
8
  last,
9
9
  upperFirst,
10
10
  map,
11
+ toNumber,
12
+ isString,
13
+ first,
14
+
11
15
  } = require('lodash');
16
+ const { validate: isUuid } = require('uuid');
17
+ const slugify = require('slugify');
12
18
  const { KIND_TYPES } = require('./utils/constant');
13
19
  const utilsFunctionsFactory = require('./utils/functions');
14
- const { additionalFields: configAdditionalFields } = require('../content-types/navigation-item').lifecycle;
15
-
20
+ const { renderType } = require('../content-types/navigation/lifecycle');
21
+ const { type: itemType, additionalFields: configAdditionalFields } = require('../content-types/navigation-item').lifecycle;
22
+ const { NotFoundError } = require('@strapi/utils').errors
16
23
  const excludedContentTypes = ['strapi::'];
17
24
  const contentTypesNameFieldsDefaults = ['title', 'subject', 'name'];
18
25
 
@@ -37,7 +44,7 @@ module.exports = ({ strapi }) => {
37
44
  const { masterModel, itemModel } = utilsFunctions.extractMeta(strapi.plugins);
38
45
  const entity = await strapi
39
46
  .query(masterModel.uid)
40
- .findOne({ where: { id }});
47
+ .findOne({ where: { id } });
41
48
 
42
49
  const entityItems = await strapi
43
50
  .query(itemModel.uid)
@@ -193,9 +200,11 @@ module.exports = ({ strapi }) => {
193
200
  .map(async ([model, related]) => {
194
201
  const relationData = await strapi
195
202
  .query(model)
196
- .findMany({where: {
197
- id: { $in: map(related, 'related_id') }
198
- }});
203
+ .findMany({
204
+ where: {
205
+ id: { $in: map(related, 'related_id') }
206
+ }
207
+ });
199
208
  return relationData
200
209
  .flatMap(_ =>
201
210
  Object.assign(
@@ -286,6 +295,263 @@ module.exports = ({ strapi }) => {
286
295
  });
287
296
  },
288
297
 
298
+ async renderChildren(
299
+ idOrSlug,
300
+ childUIKey,
301
+ type = renderType.FLAT,
302
+ menuOnly = false,
303
+ ) {
304
+ const { service } = utilsFunctions.extractMeta(strapi.plugins);
305
+ const findById = !isNaN(toNumber(idOrSlug)) || isUuid(idOrSlug);
306
+ const criteria = findById ? { id: idOrSlug } : { slug: idOrSlug };
307
+ const filter = type === renderType.FLAT ? null : childUIKey;
308
+
309
+ const itemCriteria = {
310
+ ...(menuOnly && { menuAttached: true }),
311
+ ...(type === renderType.FLAT ? { uiRouterKey: childUIKey } : {}),
312
+ };
313
+
314
+ return service.renderType(type, criteria, itemCriteria, filter);
315
+ },
316
+
317
+ async render(idOrSlug, type = renderType.FLAT, menuOnly = false) {
318
+ const { service } = utilsFunctions.extractMeta(strapi.plugins);
319
+
320
+ const findById = !isNaN(toNumber(idOrSlug)) || isUuid(idOrSlug);
321
+ const criteria = findById ? { id: idOrSlug } : { slug: idOrSlug };
322
+ const itemCriteria = menuOnly ? { menuAttached: true } : {};
323
+
324
+ return service.renderType(type, criteria, itemCriteria);
325
+ },
326
+
327
+ async renderType(type = renderType.FLAT, criteria = {}, itemCriteria = {}, filter = null) {
328
+ const { pluginName, service, masterModel, itemModel } = utilsFunctions.extractMeta(
329
+ strapi.plugins,
330
+ );
331
+
332
+ const entity = await strapi
333
+ .query(masterModel.uid)
334
+ .findOne({
335
+ where: {
336
+ ...criteria,
337
+ visible: true,
338
+ }
339
+ });
340
+ if (entity && entity.id) {
341
+ const entities = await strapi.query(itemModel.uid).findMany({
342
+ where: {
343
+ master: entity.id,
344
+ ...itemCriteria,
345
+ },
346
+ paggination: {
347
+ limit: -1,
348
+ },
349
+ sort: ['order:asc'],
350
+ populate: ['related', 'audience', 'parent'],
351
+ });
352
+
353
+ if (!entities) {
354
+ return [];
355
+ }
356
+ const items = await this.getRelatedItems(entities);
357
+ const { contentTypes, contentTypesNameFields } = await service.config();
358
+
359
+ switch (type?.toLowerCase()) {
360
+ case renderType.TREE:
361
+ case renderType.RFR:
362
+ const getTemplateName = await utilsFunctions.templateNameFactory(items, strapi, contentTypes);
363
+ const itemParser = (item, path = '', field) => {
364
+ const isExternal = item.type === itemType.EXTERNAL;
365
+ const parentPath = isExternal ? undefined : `${path === '/' ? '' : path}/${item.path === '/'
366
+ ? ''
367
+ : item.path}`;
368
+ const slug = isString(parentPath) ? slugify(
369
+ (first(parentPath) === '/' ? parentPath.substring(1) : parentPath).replace(/\//g, '-')) : undefined;
370
+ const lastRelated = item.related ? last(item.related) : undefined;
371
+ return {
372
+ id: item.id,
373
+ title: utilsFunctions.composeItemTitle(item, contentTypesNameFields, contentTypes),
374
+ menuAttached: item.menuAttached,
375
+ path: isExternal ? item.externalPath : parentPath,
376
+ type: item.type,
377
+ uiRouterKey: item.uiRouterKey,
378
+ slug: !slug && item.uiRouterKey ? slugify(item.uiRouterKey) : slug,
379
+ external: isExternal,
380
+ related: isExternal || !lastRelated ? undefined : {
381
+ ...lastRelated,
382
+ __templateName: getTemplateName(lastRelated.relatedType || lastRelated.__contentType, lastRelated.id),
383
+ },
384
+ audience: !isEmpty(item.audience) ? item.audience.map(aItem => aItem.key) : undefined,
385
+ items: isExternal ? undefined : service.renderTree({
386
+ items,
387
+ id: item.id,
388
+ field,
389
+ path: parentPath,
390
+ itemParser,
391
+ }),
392
+ };
393
+ };
394
+ const treeStructure = service.renderTree({
395
+ items,
396
+ field: 'parent',
397
+ itemParser,
398
+ });
399
+
400
+ const filteredStructure = filter
401
+ ? treeStructure.filter((item) => item.uiRouterKey === filter)
402
+ : treeStructure;
403
+
404
+ if (type === renderType.RFR) {
405
+ return service.renderRFR({
406
+ items: filteredStructure,
407
+ contentTypes,
408
+ });
409
+ }
410
+ return filteredStructure;
411
+ default:
412
+ return items
413
+ .filter(utilsFunctions.filterOutUnpublished)
414
+ .map((item) => ({
415
+ ...item,
416
+ audience: item.audience?.map(_ => _.key),
417
+ title: utilsFunctions.composeItemTitle(item, contentTypesNameFields, contentTypes),
418
+ related: item.related?.map(({ localizations, ...item }) => item),
419
+ items: null,
420
+ }));
421
+ }
422
+ }
423
+ throw new NotFoundError();
424
+ },
425
+
426
+ renderTree({
427
+ items = [],
428
+ id = null,
429
+ field = 'parent',
430
+ path = '',
431
+ itemParser = (i) => i,
432
+ }) {
433
+ return items
434
+ .filter(
435
+ (item) => {
436
+ if (item[field] === null && id === null) {
437
+ return true;
438
+ }
439
+ let data = item[field];
440
+ if (data && typeof id === 'string') {
441
+ data = data.toString();
442
+ }
443
+ return (data && data === id) || (isObject(item[field]) && (item[field].id === id));
444
+ },
445
+ )
446
+ .filter(utilsFunctions.filterOutUnpublished)
447
+ .map(item => itemParser({
448
+ ...item,
449
+ }, path, field));
450
+ },
451
+
452
+ renderRFR({ items, parent = null, parentNavItem = null, contentTypes = [] }) {
453
+ const { service } = utilsFunctions.extractMeta(strapi.plugins);
454
+ let pages = {};
455
+ let nav = {};
456
+ let navItems = [];
457
+
458
+ items.forEach(item => {
459
+ const { items: itemChilds, ...itemProps } = item;
460
+ const itemNav = service.renderRFRNav(itemProps);
461
+ const itemPage = service.renderRFRPage({
462
+ item: itemProps,
463
+ parent,
464
+ });
465
+
466
+ if (item.type === itemType.INTERNAL) {
467
+ pages = {
468
+ ...pages,
469
+ [itemPage.id]: {
470
+ ...itemPage,
471
+ },
472
+ };
473
+ }
474
+
475
+ if (item.menuAttached) {
476
+ navItems.push(itemNav);
477
+ }
478
+
479
+ if (!parent) {
480
+ nav = {
481
+ ...nav,
482
+ root: navItems,
483
+ };
484
+ } else {
485
+ const navLevel = navItems
486
+ .filter(navItem => navItem.type === itemType.INTERNAL.toLowerCase());
487
+ if (!isEmpty(navLevel))
488
+ nav = {
489
+ ...nav,
490
+ [parent]: [].concat(parentNavItem ? parentNavItem : [], navLevel),
491
+ };
492
+ }
493
+
494
+ if (!isEmpty(itemChilds)) {
495
+ const { nav: nestedNavs } = service.renderRFR({
496
+ items: itemChilds,
497
+ parent: itemPage.id,
498
+ parentNavItem: itemNav,
499
+ contentTypes,
500
+ });
501
+ const { pages: nestedPages } = service.renderRFR({
502
+ items: itemChilds.filter(child => child.type === itemType.INTERNAL),
503
+ parent: itemPage.id,
504
+ parentNavItem: itemNav,
505
+ contentTypes,
506
+ });
507
+ pages = {
508
+ ...pages,
509
+ ...nestedPages,
510
+ };
511
+ nav = {
512
+ ...nav,
513
+ ...nestedNavs,
514
+ };
515
+ }
516
+ });
517
+
518
+ return {
519
+ pages,
520
+ nav,
521
+ };
522
+ },
523
+
524
+ renderRFRNav(item) {
525
+ const { uiRouterKey, title, path, type, audience } = item;
526
+ return {
527
+ label: title,
528
+ type: type.toLowerCase(),
529
+ page: type === itemType.INTERNAL ? uiRouterKey : undefined,
530
+ url: type === itemType.EXTERNAL ? path : undefined,
531
+ audience,
532
+ };
533
+ },
534
+
535
+ renderRFRPage({ item, parent }) {
536
+ const { uiRouterKey, title, path, slug, related, type, audience, menuAttached } = item;
537
+ const { __contentType, id, __templateName } = related || {};
538
+ const contentType = __contentType || '';
539
+ return {
540
+ id: uiRouterKey,
541
+ title,
542
+ templateName: __templateName,
543
+ related: type === itemType.INTERNAL ? {
544
+ contentType,
545
+ id,
546
+ } : undefined,
547
+ path,
548
+ slug,
549
+ parent,
550
+ audience,
551
+ menuAttached,
552
+ };
553
+ },
554
+
289
555
  createBranch(items = [], masterEntity = null, parentItem = null, operations = {}) {
290
556
  const { itemModel, service } = utilsFunctions.extractMeta(strapi.plugins);
291
557
  return Promise.all(
@@ -2,9 +2,13 @@ const {
2
2
  last,
3
3
  isObject,
4
4
  isEmpty,
5
+ flatten,
6
+ find,
7
+ isString,
8
+ get,
5
9
  } = require('lodash');
6
10
 
7
- const { type: itemType } = require('../../content-types/navigation-item');
11
+ const { type: itemType } = require('../../content-types/navigation-item/lifecycle');
8
12
  const { NavigationError } = require('../../../utils/NavigationError');
9
13
  const { TEMPLATE_DEFAULT } = require('./constant');
10
14
 
@@ -13,7 +17,7 @@ module.exports = ({ strapi }) => {
13
17
  singularize(value = '') {
14
18
  return last(value) === 's' ? value.substr(0, value.length - 1) : value;
15
19
  },
16
-
20
+
17
21
  extractMeta(plugins) {
18
22
  const { navigation: plugin } = plugins;
19
23
  const { navigation: service } = plugin.services;
@@ -99,5 +103,83 @@ module.exports = ({ strapi }) => {
99
103
  return resolve();
100
104
  });
101
105
  },
106
+
107
+ async templateNameFactory(items, strapi, contentTypes = []) {
108
+ const flatRelated = flatten(items.map(i => i.related)).filter(_ => !!_);
109
+ const relatedMap = flatRelated.reduce((acc, curr) => {
110
+ if (!acc[curr.__contentType]) {
111
+ acc[curr.__contentType] = [];
112
+ }
113
+ acc[curr.__contentType].push(curr.id);
114
+ return acc;
115
+ }, {});
116
+ const responses = await Promise.all(
117
+ Object.entries(relatedMap)
118
+ .map(
119
+ ([contentType, ids]) => {
120
+ const contentTypeUid = get(find(contentTypes, cnt => cnt.uid === contentType), 'uid');
121
+ return strapi.query(contentTypeUid)
122
+ .findMany({ id_in: ids, _limit: -1 })
123
+ .then(res => ({ [contentType]: res }))
124
+ }),
125
+ );
126
+ const relatedResponseMap = responses.reduce((acc, curr) => ({ ...acc, ...curr }), {});
127
+ const singleTypes = new Map(
128
+ contentTypes
129
+ .filter(x => x.isSingle)
130
+ .map(({ contentTypeName, templateName }) => [contentTypeName, templateName || contentTypeName])
131
+ );
132
+
133
+ return (contentType, id) => {
134
+ const template = get(relatedResponseMap[contentType].find(data => data.id === id), 'template');
135
+
136
+ if (template) {
137
+ const templateComponent = this.getTemplateComponentFromTemplate(template);
138
+ return get(templateComponent, 'options.templateName', TEMPLATE_DEFAULT);
139
+ }
140
+
141
+ if (singleTypes.get(contentType)) {
142
+ return singleTypes.get(contentType);
143
+ }
144
+
145
+ return TEMPLATE_DEFAULT;
146
+ };
147
+ },
148
+
149
+ getTemplateComponentFromTemplate(template = []) {
150
+ const componentName = get(first(template), '__component');
151
+ return componentName ? strapi.components[componentName] : null;
152
+ },
153
+
154
+ composeItemTitle(item = {}, fields = {}, contentTypes = []) {
155
+ const { title, related } = item;
156
+ if (title) {
157
+ return isString(title) && !isEmpty(title) ? title : undefined;
158
+ } else if (related) {
159
+ const relationTitle = this.extractItemRelationTitle(isArray(related) ? last(related) : related, fields, { contentTypes });
160
+ return isString(relationTitle) && !isEmpty(relationTitle) ? relationTitle : undefined;
161
+ }
162
+ return undefined;
163
+ },
164
+
165
+ extractItemRelationTitle(relatedItem = {}, fields = {}, contentTypes = []) {
166
+ const { __contentType } = relatedItem;
167
+ const contentType = find(contentTypes, _ => _.contentTypeName === __contentType);
168
+ const { default: defaultFields = [] } = fields;
169
+ return get(fields, `${contentType ? contentType.collectionName : ''}`, defaultFields).map((_) => relatedItem[_]).filter((_) => _)[0] || '';
170
+ },
171
+
172
+ filterOutUnpublished(item) {
173
+ const relatedItem = item.related && last(item.related);
174
+ const isHandledByPublshFlow = relatedItem ? 'published_at' in relatedItem : false;
175
+
176
+ if (isHandledByPublshFlow) {
177
+ const isRelatedDefinedAndPublished = relatedItem ?
178
+ isHandledByPublshFlow && get(relatedItem, 'published_at') :
179
+ false;
180
+ return item.type === itemType.INTERNAL ? isRelatedDefinedAndPublished : true;
181
+ }
182
+ return (item.type === itemType.EXTERNAL) || relatedItem;
183
+ },
102
184
  };
103
185
  }
package/strapi-server.js CHANGED
@@ -4,6 +4,7 @@ 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');
7
8
 
8
9
 
9
10
  module.exports = () => {
@@ -13,6 +14,7 @@ module.exports = () => {
13
14
  routes,
14
15
  controllers,
15
16
  services,
16
- contentTypes
17
+ contentTypes,
18
+ register,
17
19
  };
18
20
  };