apostrophe 3.17.0 → 3.18.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.
Files changed (97) hide show
  1. package/.editorconfig +3 -0
  2. package/.eslintrc +4 -3
  3. package/.github/workflows/main.yml +2 -2
  4. package/.stylelintrc +12 -2
  5. package/CHANGELOG.md +34 -2
  6. package/defaults.js +2 -2
  7. package/index.js +124 -33
  8. package/lib/escape-host.js +8 -0
  9. package/lib/mongodb-connect.js +55 -0
  10. package/lib/opentelemetry.js +144 -0
  11. package/modules/@apostrophecms/area/ui/apos/apps/AposAreas.js +2 -0
  12. package/modules/@apostrophecms/area/ui/apos/components/AposAreaEditor.vue +20 -8
  13. package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +10 -0
  14. package/modules/@apostrophecms/asset/lib/globalIcons.js +1 -0
  15. package/modules/@apostrophecms/attachment/index.js +81 -29
  16. package/modules/@apostrophecms/db/index.js +7 -10
  17. package/modules/@apostrophecms/doc/index.js +138 -23
  18. package/modules/@apostrophecms/doc-type/index.js +162 -63
  19. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocContextMenu.vue +39 -1
  20. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +11 -1
  21. package/modules/@apostrophecms/email/index.js +1 -1
  22. package/modules/@apostrophecms/express/index.js +2 -2
  23. package/modules/@apostrophecms/http/index.js +2 -1
  24. package/modules/@apostrophecms/i18n/i18n/en.json +10 -0
  25. package/modules/@apostrophecms/i18n/i18n/es.json +7 -0
  26. package/modules/@apostrophecms/i18n/i18n/pt-BR.json +7 -0
  27. package/modules/@apostrophecms/i18n/i18n/sk.json +7 -0
  28. package/modules/@apostrophecms/image/index.js +182 -1
  29. package/modules/@apostrophecms/image/ui/apos/apps/AposImageRelationshipQueryFilter.js +13 -0
  30. package/modules/@apostrophecms/image/ui/apos/components/AposImageCropper.vue +460 -0
  31. package/modules/@apostrophecms/image/ui/apos/components/AposImageRelationshipEditor.vue +510 -0
  32. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManager.vue +5 -1
  33. package/modules/@apostrophecms/image/ui/apos/lib/aspectRatios.js +26 -0
  34. package/modules/@apostrophecms/image-widget/views/widget.html +5 -2
  35. package/modules/@apostrophecms/modal/ui/apos/mixins/AposEditorMixin.js +45 -1
  36. package/modules/@apostrophecms/module/index.js +98 -17
  37. package/modules/@apostrophecms/module/lib/events.js +46 -11
  38. package/modules/@apostrophecms/page/index.js +55 -22
  39. package/modules/@apostrophecms/piece-page-type/index.js +1 -0
  40. package/modules/@apostrophecms/piece-type/index.js +13 -4
  41. package/modules/@apostrophecms/piece-type/ui/apos/components/AposRelationshipEditor.vue +2 -2
  42. package/modules/@apostrophecms/rich-text-widget/index.js +1 -3
  43. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +4 -0
  44. package/modules/@apostrophecms/schema/index.js +79 -73
  45. package/modules/@apostrophecms/schema/ui/apos/components/AposInputArea.vue +10 -0
  46. package/modules/@apostrophecms/schema/ui/apos/components/AposInputObject.vue +22 -3
  47. package/modules/@apostrophecms/schema/ui/apos/components/AposInputRelationship.vue +72 -36
  48. package/modules/@apostrophecms/schema/ui/apos/components/AposInputSelect.vue +7 -26
  49. package/modules/@apostrophecms/schema/ui/apos/components/AposInputString.vue +8 -0
  50. package/modules/@apostrophecms/schema/ui/apos/components/AposSchema.vue +45 -15
  51. package/modules/@apostrophecms/task/index.js +106 -52
  52. package/modules/@apostrophecms/template/index.js +111 -76
  53. package/modules/@apostrophecms/template/lib/custom-tags/component.js +42 -22
  54. package/modules/@apostrophecms/ui/ui/apos/components/AposSelect.vue +61 -0
  55. package/modules/@apostrophecms/ui/ui/apos/components/AposSlat.vue +46 -11
  56. package/modules/@apostrophecms/ui/ui/apos/components/AposSlatList.vue +10 -0
  57. package/modules/@apostrophecms/ui/ui/apos/components/AposTreeHeader.vue +2 -22
  58. package/modules/@apostrophecms/ui/ui/apos/utils/index.js +9 -0
  59. package/modules/@apostrophecms/widget-type/index.js +2 -23
  60. package/modules/@apostrophecms/widget-type/ui/apos/components/AposWidget.vue +1 -1
  61. package/modules/@apostrophecms/widget-type/ui/apos/components/AposWidgetEditor.vue +20 -1
  62. package/modules/@apostrophecms/widget-type/ui/apos/mixins/AposWidgetMixin.js +0 -9
  63. package/package.json +16 -12
  64. package/scripts/lint-i18n.js +2 -2
  65. package/test/assets.js +2 -1
  66. package/test/attachments.js +119 -26
  67. package/test/bundle.js +1 -1
  68. package/test/content-i18n.js +6 -6
  69. package/test/docs.js +244 -4
  70. package/test/draft-published.js +41 -41
  71. package/test/express.js +1 -1
  72. package/test/http.js +2 -2
  73. package/test/images.js +94 -4
  74. package/test/job.js +1 -1
  75. package/test/locks.js +1 -1
  76. package/test/middleware-and-route-order.js +3 -3
  77. package/test/pages-public-api.js +48 -4
  78. package/test/pages-rest.js +20 -20
  79. package/test/pages.js +377 -11
  80. package/test/parked-pages.js +1 -1
  81. package/test/permissions.js +10 -10
  82. package/test/pieces-public-api.js +130 -6
  83. package/test/pieces.js +247 -60
  84. package/test/recursionGuard.js +6 -6
  85. package/test/restApiRoutes.js +6 -6
  86. package/test/schemaBuilders.js +7 -7
  87. package/test/schemas.js +59 -59
  88. package/test/search.js +3 -3
  89. package/test/soft-redirects.js +13 -13
  90. package/test/static-i18n.js +1 -1
  91. package/test/templates.js +10 -10
  92. package/test/urls.js +2 -2
  93. package/test/users.js +21 -21
  94. package/test/utils.js +13 -13
  95. package/test/widgets.js +2 -2
  96. package/test-lib/util.js +2 -5
  97. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidget.vue +0 -26
@@ -15,62 +15,78 @@
15
15
 
16
16
  const _ = require('lodash');
17
17
  const { stripIndent } = require('common-tags');
18
+ const { SemanticAttributes } = require('@opentelemetry/semantic-conventions');
18
19
 
19
20
  module.exports = {
20
21
  options: { alias: 'task' },
21
22
  handlers(self) {
22
23
  return {
23
24
  'apostrophe:run': {
24
- async runTask(isTask) {
25
+ async runTask(isTask, after) {
25
26
 
26
27
  if (!isTask) {
27
28
  return;
28
29
  }
29
30
 
30
- let task;
31
31
  const cmd = self.apos.argv._[0];
32
- if (!cmd) {
33
- throw new Error('There is no command line argument to serve as a task name, should never happen');
34
- }
35
-
36
- if (cmd === 'help') {
32
+ const telemetry = self.apos.telemetry;
33
+ const spanName = `task:${cmd}`;
34
+ await telemetry.startActiveSpan(spanName, async (span) => {
35
+ span.setAttribute(SemanticAttributes.CODE_FUNCTION, 'runTask');
36
+ span.setAttribute(SemanticAttributes.CODE_NAMESPACE, '@apostrophecms/task');
37
+ span.setAttribute(telemetry.Attributes.ARGV, telemetry.stringify(self.apos.argv));
37
38
 
38
- // list all tasks
39
- if (self.apos.argv._.length === 1) {
40
- self.usage();
39
+ let task;
40
+ if (!cmd) {
41
+ const err = 'There is no command line argument to serve as a task name, should never happen';
42
+ console.error(err);
43
+ return self.exit(after, 1, span, err);
41
44
  }
42
45
 
43
- // help with specific task
44
- if (self.apos.argv._.length === 2) {
45
- task = self.find(self.apos.argv._[1]);
46
- if (!task) {
47
- console.error('There is no such task.');
48
- self.usage();
46
+ if (cmd === 'help') {
47
+ span.setAttribute(telemetry.Attributes.TARGET_FUNCTION, 'help');
48
+ // list all tasks
49
+ if (self.apos.argv._.length === 1) {
50
+ return self.usage(after, span);
49
51
  }
50
- if (task.usage) {
51
- console.log(`\nTips for the ${task.fullName} task:\n`);
52
- console.log(task.usage);
53
- } else {
54
- console.log('That is a valid task, but it does not have a help message.');
52
+
53
+ // help with specific task
54
+ if (self.apos.argv._.length === 2) {
55
+ span.setAttribute(telemetry.Attributes.TARGET_NAMESPACE, self.apos.argv._[1]);
56
+ task = self.find(self.apos.argv._[1]);
57
+ if (!task) {
58
+ console.error('There is no such task.');
59
+ return self.usage(after, span, 'There is no such task.');
60
+ }
61
+ if (task.usage) {
62
+ console.log(`\nTips for the ${task.fullName} task:\n`);
63
+ console.log(task.usage);
64
+ } else {
65
+ console.log('That is a valid task, but it does not have a help message.');
66
+ }
67
+ return self.exit(after, 0, span);
55
68
  }
56
- process.exit(0);
57
69
  }
58
- }
59
70
 
60
- task = self.find(cmd);
71
+ task = self.find(cmd);
61
72
 
62
- if (!task) {
63
- console.error('\nThere is no such task.');
64
- self.usage();
65
- }
73
+ if (!task) {
74
+ console.error('\nThere is no such task.');
75
+ return self.usage(after, span, `There is no such task ${cmd}`);
76
+ }
66
77
 
67
- try {
68
- await task.task(self.apos.argv);
69
- } catch (e) {
70
- console.error(e);
71
- process.exit(1);
72
- }
73
- process.exit(0);
78
+ const [ moduleName, taskName ] = task.fullName.split(':');
79
+ span.setAttribute(telemetry.Attributes.TARGET_NAMESPACE, moduleName);
80
+ span.setAttribute(telemetry.Attributes.TARGET_FUNCTION, taskName);
81
+
82
+ try {
83
+ await task.task(self.apos.argv);
84
+ return self.exit(after, 0, span);
85
+ } catch (e) {
86
+ console.error(e);
87
+ return self.exit(after, 1, span, e);
88
+ }
89
+ });
74
90
  }
75
91
  }
76
92
  };
@@ -116,21 +132,39 @@ module.exports = {
116
132
  // task developer might assume they can exit the process directly.
117
133
 
118
134
  async invoke(name, args, options) {
119
- const aposArgv = self.apos.argv;
120
- if (Array.isArray(args)) {
121
- args.splice(0, 0, name);
122
- } else {
123
- options = args;
124
- args = [ name ];
125
- }
126
- const task = self.find(name);
127
- const argv = {
128
- _: args,
129
- ...options || {}
130
- };
131
- self.apos.argv = argv;
132
- await task.task(argv);
133
- self.apos.argv = aposArgv;
135
+ const telemetry = self.apos.telemetry;
136
+ const spanName = `task:${self.__meta.name}:${name}`;
137
+ await telemetry.startActiveSpan(spanName, async (span) => {
138
+ span.setAttribute(SemanticAttributes.CODE_FUNCTION, 'invoke');
139
+ span.setAttribute(SemanticAttributes.CODE_NAMESPACE, '@apostrophecms/task');
140
+ try {
141
+ const aposArgv = self.apos.argv;
142
+ if (Array.isArray(args)) {
143
+ args.splice(0, 0, name);
144
+ } else {
145
+ options = args;
146
+ args = [ name ];
147
+ }
148
+ const task = self.find(name);
149
+ const [ moduleName, taskName ] = task.fullName.split(':');
150
+ span.setAttribute(telemetry.Attributes.TARGET_NAMESPACE, moduleName);
151
+ span.setAttribute(telemetry.Attributes.TARGET_FUNCTION, taskName);
152
+ const argv = {
153
+ _: args,
154
+ ...options || {}
155
+ };
156
+ span.setAttribute(telemetry.Attributes.ARGV, telemetry.stringify(argv));
157
+ self.apos.argv = argv;
158
+ await task.task(argv);
159
+ self.apos.argv = aposArgv;
160
+ span.setStatus({ code: telemetry.api.SpanStatusCode.OK });
161
+ } catch (err) {
162
+ telemetry.handleError(span, err);
163
+ throw err;
164
+ } finally {
165
+ span.end();
166
+ }
167
+ });
134
168
  },
135
169
 
136
170
  // Identifies the task corresponding to the given command line argument.
@@ -152,8 +186,9 @@ module.exports = {
152
186
 
153
187
  // Displays a usage message, including a list of available tasks,
154
188
  // and exits the entire program with a nonzero status code.
189
+ // Forward signal, span and error to the exit handler.
155
190
 
156
- usage() {
191
+ usage(after, span, err) {
157
192
  // Direct use of console makes sense in tasks. -Tom
158
193
  console.error('\nThe following tasks are available:\n');
159
194
  for (const [ moduleName, module ] of Object.entries(self.apos.modules)) {
@@ -165,7 +200,26 @@ module.exports = {
165
200
  console.error('node app help groupname:taskname\n');
166
201
  console.error('To get help with a specific task.\n');
167
202
  console.error('To launch the site, run with no arguments.');
168
- process.exit(1);
203
+ return self.exit(after, 1, span, err);
204
+ },
205
+
206
+ // Register error (if any) and close the current telemetry span;
207
+ // send a signal back to the bootstrap to exit the process with a given code.
208
+
209
+ exit(after, code, span, err) {
210
+ after.exit = code;
211
+ if (!span) {
212
+ return;
213
+ }
214
+
215
+ if (err) {
216
+ self.apos.telemetry.handleError(span, err);
217
+ } else if (code) {
218
+ span.setStatus({ code: self.apos.telemetry.api.SpanStatusCode.ERROR });
219
+ } else {
220
+ span.setStatus({ code: self.apos.telemetry.api.SpanStatusCode.OK });
221
+ }
222
+ span.end();
169
223
  },
170
224
 
171
225
  // Return a `req` object suitable for command line tasks
@@ -31,6 +31,7 @@ const qs = require('qs');
31
31
  const Promise = require('bluebird');
32
32
  const path = require('path');
33
33
  const { stripIndent } = require('common-tags');
34
+ const { SemanticAttributes } = require('@opentelemetry/semantic-conventions');
34
35
 
35
36
  module.exports = {
36
37
  options: { alias: 'template' },
@@ -631,91 +632,125 @@ module.exports = {
631
632
  // async function.
632
633
 
633
634
  async renderPageForModule(req, template, data, module) {
634
- let scene = req.user ? 'apos' : 'public';
635
- if (req.scene) {
636
- scene = req.scene;
637
- } else {
638
- req.scene = scene;
639
- }
635
+ const telemetry = self.apos.telemetry;
636
+ const spanName = `render:${self.__meta.name}:renderPageForModule`;
637
+ return telemetry.startActiveSpan(spanName, async (span) => {
638
+ span.setAttributes({
639
+ [SemanticAttributes.CODE_FUNCTION]: 'renderPageForModule',
640
+ [SemanticAttributes.CODE_NAMESPACE]: self.__meta.name
641
+ });
640
642
 
641
- const aposBodyData = {
642
- modules: {},
643
- prefix: req.prefix,
644
- sitePrefix: self.apos.prefix,
645
- shortName: self.apos.shortName,
646
- locale: req.locale,
647
- csrfCookieName: self.apos.csrfCookieName,
648
- tabId: self.apos.util.generateId(),
649
- uploadsUrl: self.apos.attachment.uploadfs.getUrl(),
650
- assetBaseUrl: self.apos.asset.getAssetBaseUrl(),
651
- scene
652
- };
653
- if (req.user) {
654
- aposBodyData.user = {
655
- title: req.user.title,
656
- _id: req.user._id,
657
- username: req.user.username
643
+ let scene = req.user ? 'apos' : 'public';
644
+ if (req.scene) {
645
+ scene = req.scene;
646
+ } else {
647
+ req.scene = scene;
648
+ }
649
+ span.setAttribute(telemetry.Attributes.SCENE, scene);
650
+ span.setAttribute(telemetry.Attributes.TEMPLATE, template);
651
+
652
+ const aposBodyData = {
653
+ modules: {},
654
+ prefix: req.prefix,
655
+ sitePrefix: self.apos.prefix,
656
+ shortName: self.apos.shortName,
657
+ locale: req.locale,
658
+ csrfCookieName: self.apos.csrfCookieName,
659
+ tabId: self.apos.util.generateId(),
660
+ uploadsUrl: self.apos.attachment.uploadfs.getUrl(),
661
+ assetBaseUrl: self.apos.asset.getAssetBaseUrl(),
662
+ scene
663
+ };
664
+ if (req.user) {
665
+ aposBodyData.user = {
666
+ title: req.user.title,
667
+ _id: req.user._id,
668
+ username: req.user.username
669
+ };
670
+ }
671
+ await self.emit('addBodyData', req, aposBodyData);
672
+ self.addBodyDataAttribute(req, { apos: JSON.stringify(aposBodyData) });
673
+
674
+ // Always the last call; signifies we're done initializing the
675
+ // page as far as the core is concerned; a lovely time for other
676
+ // modules and project-level javascript to do their own
677
+ // enhancements.
678
+ //
679
+ // This method emits a 'ready' event, and also
680
+ // emits an 'enhance' event with the entire $body
681
+ // as its argument.
682
+ //
683
+ // Waits for DOMready to give other
684
+ // things maximum opportunity to happen.
685
+
686
+ const decorate = req.query.aposRefresh !== '1';
687
+
688
+ // data.url will be the original requested page URL, for use in building
689
+ // relative links, adding or removing query parameters, etc. If this is a
690
+ // refresh request, we remove that so that frontend templates don't build
691
+ // URLs that also refresh
692
+
693
+ const args = {
694
+ outerLayout: decorate ? '@apostrophecms/template:outerLayout.html' : '@apostrophecms/template:refreshLayout.html',
695
+ permissions: req.user && (req.user._permissions || {}),
696
+ scene,
697
+ refreshing: !decorate,
698
+ // Make the query available to templates for easy access to
699
+ // filter settings etc.
700
+ query: req.query,
701
+ url: unrefreshed(req.url)
658
702
  };
659
- }
660
- await self.emit('addBodyData', req, aposBodyData);
661
- self.addBodyDataAttribute(req, { apos: JSON.stringify(aposBodyData) });
662
-
663
- // Always the last call; signifies we're done initializing the
664
- // page as far as the core is concerned; a lovely time for other
665
- // modules and project-level javascript to do their own
666
- // enhancements.
667
- //
668
- // This method emits a 'ready' event, and also
669
- // emits an 'enhance' event with the entire $body
670
- // as its argument.
671
- //
672
- // Waits for DOMready to give other
673
- // things maximum opportunity to happen.
674
-
675
- const decorate = req.query.aposRefresh !== '1';
676
-
677
- // data.url will be the original requested page URL, for use in building
678
- // relative links, adding or removing query parameters, etc. If this is a
679
- // refresh request, we remove that so that frontend templates don't build
680
- // URLs that also refresh
681
-
682
- const args = {
683
- outerLayout: decorate ? '@apostrophecms/template:outerLayout.html' : '@apostrophecms/template:refreshLayout.html',
684
- permissions: req.user && (req.user._permissions || {}),
685
- scene,
686
- refreshing: !decorate,
687
- // Make the query available to templates for easy access to
688
- // filter settings etc.
689
- query: req.query,
690
- url: unrefreshed(req.url)
691
- };
692
703
 
693
- _.extend(args, data);
704
+ _.extend(args, data);
694
705
 
695
- if (req.aposError) {
706
+ if (req.aposError) {
696
707
  // A 500-worthy error occurred already, i.e. in `pageBeforeSend`
697
- return error(req.aposError);
698
- }
699
-
700
- try {
701
- const content = await module.render(req, template, args);
708
+ telemetry.handleError(span, req.aposError);
709
+ span.end();
710
+ return error(req.aposError);
711
+ }
702
712
 
703
- const filledContent = self.insertBundlesMarkup({
704
- page: req.data.bestPage,
705
- scene,
706
- template,
707
- content,
708
- scriptsPlaceholder: req.scriptsPlaceholder,
709
- stylesheetsPlaceholder: req.stylesheetsPlaceholder,
710
- widgetsBundles: req.widgetsBundles
711
- });
713
+ try {
714
+ const spanRenderName = `render:${module.__meta.name}:render`;
715
+ const content = await telemetry.startActiveSpan(spanRenderName, async (spanRender) => {
716
+ spanRender.setAttribute(SemanticAttributes.CODE_FUNCTION, 'render');
717
+ spanRender.setAttribute(SemanticAttributes.CODE_NAMESPACE, module.__meta.name);
718
+ spanRender.setAttribute(telemetry.Attributes.SCENE, scene);
719
+ spanRender.setAttribute(telemetry.Attributes.TEMPLATE, template);
720
+
721
+ try {
722
+ const content = await module.render(req, template, args);
723
+ spanRender.setStatus({ code: telemetry.api.SpanStatusCode.OK });
724
+ return content;
725
+ } catch (err) {
726
+ telemetry.handleError(spanRender, err);
727
+ throw err;
728
+ } finally {
729
+ spanRender.end();
730
+ }
731
+ }, span);
732
+
733
+ const filledContent = self.insertBundlesMarkup({
734
+ page: req.data.bestPage,
735
+ scene,
736
+ template,
737
+ content,
738
+ scriptsPlaceholder: req.scriptsPlaceholder,
739
+ stylesheetsPlaceholder: req.stylesheetsPlaceholder,
740
+ widgetsBundles: req.widgetsBundles
741
+ });
712
742
 
713
- return filledContent;
714
- } catch (e) {
743
+ span.setStatus({ code: telemetry.api.SpanStatusCode.OK });
744
+ return filledContent;
745
+ } catch (e) {
715
746
  // The page template threw an exception. Log where it
716
747
  // occurred for easier debugging
717
- return error(e);
718
- }
748
+ telemetry.handleError(span, e);
749
+ return error(e);
750
+ } finally {
751
+ span.end();
752
+ }
753
+ });
719
754
 
720
755
  function error(e) {
721
756
  self.logError(req, e);
@@ -1,5 +1,7 @@
1
1
  // Implements {% component 'moduleName:componentName' with dataObject %}
2
2
 
3
+ const { SemanticAttributes } = require('@opentelemetry/semantic-conventions');
4
+
3
5
  module.exports = function(self) {
4
6
  return {
5
7
  // We need a custom parser because of the "with" syntax
@@ -27,29 +29,47 @@ module.exports = function(self) {
27
29
  if (!data) {
28
30
  data = {};
29
31
  }
30
- const parsed = name.match(/^([^:]+):(.+)$/);
31
- if (!parsed) {
32
- throw new Error('When using {% component %} the component name must be a string like: module-name:componentName');
33
- }
34
- // eslint-disable-next-line no-unused-vars
35
- const [ dummy, moduleName, componentName ] = parsed;
36
- const module = self.apos.modules[moduleName];
37
- if (!module) {
38
- throw new Error(`{% component %} was invoked with the name of a module that does not exist. Hint:\nit must be a module that is actually live in your project, not a base class\nlike @apostrophecms/piece-type.\nModule name: ${moduleName} Component name: ${componentName}`);
39
- }
40
- if (!(module.components && module.components[componentName])) {
41
- throw new Error(`{% component %} was invoked with the name of a component that does not exist.\nModule name: ${moduleName} Component name: ${componentName}`);
42
- }
43
- const result = await self.apos.util.recursionGuard(req, `component:${moduleName}:${componentName}`, async () => {
44
- const input = await module.components[componentName](req, data);
45
- return module.render(req, componentName, input);
32
+
33
+ const telemetry = self.apos.telemetry;
34
+ const spanName = `component:${name}`;
35
+ return telemetry.startActiveSpan(spanName, async (span) => {
36
+ span.setAttribute(telemetry.Attributes.TEMPLATE, name);
37
+
38
+ try {
39
+ const parsed = name.match(/^([^:]+):(.+)$/);
40
+ if (!parsed) {
41
+ throw new Error('When using {% component %} the component name must be a string like: module-name:componentName');
42
+ }
43
+ // eslint-disable-next-line no-unused-vars
44
+ const [ dummy, moduleName, componentName ] = parsed;
45
+ span.setAttribute(SemanticAttributes.CODE_FUNCTION, componentName);
46
+ span.setAttribute(SemanticAttributes.CODE_NAMESPACE, moduleName);
47
+ const module = self.apos.modules[moduleName];
48
+ if (!module) {
49
+ throw new Error(`{% component %} was invoked with the name of a module that does not exist. Hint:\nit must be a module that is actually live in your project, not a base class\nlike @apostrophecms/piece-type.\nModule name: ${moduleName} Component name: ${componentName}`);
50
+ }
51
+ if (!(module.components && module.components[componentName])) {
52
+ throw new Error(`{% component %} was invoked with the name of a component that does not exist.\nModule name: ${moduleName} Component name: ${componentName}`);
53
+ }
54
+ const result = await self.apos.util.recursionGuard(req, `component:${moduleName}:${componentName}`, async () => {
55
+ const input = await module.components[componentName](req, data);
56
+ return module.render(req, componentName, input);
57
+ });
58
+ span.setStatus({ code: telemetry.api.SpanStatusCode.OK });
59
+
60
+ if (result === undefined) {
61
+ // Recursion guard stopped it, nunjucks expects a string
62
+ return '';
63
+ } else {
64
+ return result;
65
+ }
66
+ } catch (err) {
67
+ telemetry.handleError(span, err);
68
+ throw err;
69
+ } finally {
70
+ span.end();
71
+ }
46
72
  });
47
- if (result === undefined) {
48
- // Recursion guard stopped it, nunjucks expects a string
49
- return '';
50
- } else {
51
- return result;
52
- }
53
73
  }
54
74
  };
55
75
  };
@@ -0,0 +1,61 @@
1
+ <template>
2
+ <div class="apos-input-wrapper">
3
+ <select
4
+ class="apos-input apos-input--select"
5
+ :disabled="disabled"
6
+ @change="change($event.target.value)"
7
+ >
8
+ <option
9
+ v-for="choice in choices"
10
+ :key="JSON.stringify(choice.value)"
11
+ :value="JSON.stringify(choice.value)"
12
+ :selected="choice.value === selected"
13
+ >
14
+ {{ $t(choice.label) }}
15
+ </option>
16
+ </select>
17
+ <AposIndicator
18
+ icon="menu-down-icon"
19
+ class="apos-input-icon"
20
+ :icon-size="20"
21
+ />
22
+ </div>
23
+ </template>
24
+ <script>
25
+
26
+ export default {
27
+ name: 'AposSelect',
28
+ props: {
29
+ icon: {
30
+ type: String,
31
+ default: 'menu-down-icon'
32
+ },
33
+ choices: {
34
+ type: Array,
35
+ default() {
36
+ return [];
37
+ }
38
+ },
39
+ selected: {
40
+ type: [ String, Number ],
41
+ default: ''
42
+ },
43
+ disabled: {
44
+ type: Boolean,
45
+ default: false
46
+ }
47
+ },
48
+ emits: [ 'change' ],
49
+ methods: {
50
+ change(value) {
51
+ this.$emit('change', value);
52
+ }
53
+ }
54
+ };
55
+ </script>
56
+
57
+ <style lang="scss" scoped>
58
+ .apos-input-icon {
59
+ @include apos-transition();
60
+ }
61
+ </style>
@@ -27,13 +27,26 @@
27
27
  :size="13"
28
28
  />
29
29
  <AposContextMenu
30
- v-if="hasRelationshipSchema"
30
+ v-if="hasRelationshipEditor && more.menu.length"
31
31
  :button="more.button"
32
32
  :menu="more.menu"
33
33
  @item-clicked="$emit('item-clicked', item)"
34
34
  menu-placement="bottom-start"
35
35
  menu-offset="40, 10"
36
36
  />
37
+ <AposButton
38
+ class="apos-slat__editor-btn"
39
+ v-if="editorIcon && hasRelationshipEditor"
40
+ role="button"
41
+ :tooltip="{
42
+ content: editorLabel,
43
+ placement: 'bottom'
44
+ }"
45
+ :icon="editorIcon"
46
+ :icon-only="true"
47
+ :modifiers="['inline']"
48
+ @click="$emit('item-clicked', item)"
49
+ />
37
50
  <a
38
51
  class="apos-slat__control apos-slat__control--view"
39
52
  v-if="item._url || item._urls"
@@ -42,9 +55,16 @@
42
55
  >
43
56
  <eye-icon :size="14" class="apos-slat__control--view-icon" />
44
57
  </a>
45
- <div v-if="item.attachment && item.attachment.group === 'images' && item.attachment._urls" class="apos-slat__media-preview">
58
+ <div
59
+ v-if="item.attachment &&
60
+ item.attachment.group === 'images' &&
61
+ item.attachment._urls"
62
+ class="apos-slat__media-preview"
63
+ >
46
64
  <img
47
- :src="item.attachment._urls['one-sixth']"
65
+ :src="item.attachment._urls.uncropped
66
+ ? item.attachment._urls.uncropped['one-sixth']
67
+ : item.attachment._urls['one-sixth']"
48
68
  :alt="item.description || item.title"
49
69
  class="apos-slat__media"
50
70
  >
@@ -114,6 +134,14 @@ export default {
114
134
  hasRelationshipSchema: {
115
135
  type: Boolean,
116
136
  default: false
137
+ },
138
+ editorLabel: {
139
+ type: String,
140
+ default: null
141
+ },
142
+ editorIcon: {
143
+ type: String,
144
+ default: null
117
145
  }
118
146
  },
119
147
  emits: [ 'engage', 'disengage', 'move', 'remove', 'item-clicked', 'select' ],
@@ -128,10 +156,10 @@ export default {
128
156
  type: 'inline'
129
157
  },
130
158
  menu: [
131
- {
132
- label: 'Edit Relationship',
159
+ ...!this.editorIcon ? [ {
160
+ label: 'apostrophe:editRelationship',
133
161
  action: 'edit-relationship'
134
- }
162
+ } ] : []
135
163
  ]
136
164
  }
137
165
  };
@@ -144,6 +172,12 @@ export default {
144
172
  } else {
145
173
  return `${(size / 1000000).toFixed(1)}MB`;
146
174
  }
175
+ },
176
+ hasRelationshipEditor() {
177
+ if (this.item.attachment && this.item.attachment.group === 'images') {
178
+ return this.hasRelationshipSchema && this.item.attachment._isCroppable;
179
+ }
180
+ return this.hasRelationshipSchema;
147
181
  }
148
182
  },
149
183
  methods: {
@@ -162,11 +196,8 @@ export default {
162
196
  },
163
197
  move(dir) {
164
198
  if (this.engaged) {
165
- if (dir > 0) {
166
- this.$emit('move', this.item._id, 1);
167
- } else {
168
- this.$emit('move', this.item._id, -1);
169
- }
199
+ const direction = dir > 0 ? 1 : -1;
200
+ this.$emit('move', this.item._id, direction);
170
201
  }
171
202
  },
172
203
  remove(focusNext) {
@@ -259,6 +290,10 @@ export default {
259
290
  text-overflow: ellipsis;
260
291
  }
261
292
 
293
+ .apos-slat__editor-btn {
294
+ margin-right: 5px;
295
+ }
296
+
262
297
  .apos-slat__control {
263
298
  display: flex;
264
299
  align-content: center;