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
package/.editorconfig CHANGED
@@ -9,3 +9,6 @@ indent_size = 2
9
9
  indent_style = space
10
10
  insert_final_newline = true
11
11
  trim_trailing_whitespace = true
12
+
13
+ [*.md]
14
+ trim_trailing_whitespace = false
package/.eslintrc CHANGED
@@ -79,7 +79,8 @@
79
79
  ],
80
80
  "parser": "vue-eslint-parser",
81
81
  "parserOptions": {
82
- "parser": "babel-eslint",
83
- "sourceType": "module"
82
+ "parser": "@babel/eslint-parser",
83
+ "sourceType": "module",
84
+ "requireConfigFile": false
84
85
  }
85
- }
86
+ }
@@ -20,8 +20,8 @@ jobs:
20
20
  runs-on: ubuntu-latest
21
21
  strategy:
22
22
  matrix:
23
- node-version: [12, 14, 16]
24
- mongodb-version: [4.2, 4.4, 5.0]
23
+ node-version: [14, 16, 18]
24
+ mongodb-version: [4.2, 5.0]
25
25
 
26
26
  # Steps represent a sequence of tasks that will be executed as part of the job
27
27
  steps:
package/.stylelintrc CHANGED
@@ -6,7 +6,7 @@
6
6
  'color-hex-length': 'short',
7
7
  'color-named': 'never',
8
8
  'color-no-invalid-hex': true,
9
- 'declaration-property-unit-whitelist': { 'line-height': [] },
9
+ 'declaration-property-unit-allowed-list': { 'line-height': [] },
10
10
  'font-family-no-duplicate-names': true,
11
11
  'number-leading-zero': 'always',
12
12
  'number-max-precision': 2,
@@ -80,5 +80,15 @@
80
80
  ]
81
81
  }
82
82
  ]
83
- }
83
+ },
84
+ overrides: [
85
+ {
86
+ files: ['**/*.scss'],
87
+ customSyntax: 'postcss-scss'
88
+ },
89
+ {
90
+ files: ['**/*.vue'],
91
+ customSyntax: 'postcss-html'
92
+ }
93
+ ]
84
94
  }
package/CHANGELOG.md CHANGED
@@ -1,6 +1,33 @@
1
1
  # Changelog
2
2
 
3
- # 3.17.0 (2022-03-31)
3
+ # 3.18.0 (2022-05-03)
4
+
5
+ ### Adds
6
+
7
+ * Images may now be cropped to suit a particular placement after selecting them. SVG files may not be cropped as it is not possible in the general case.
8
+ * Editors may also select a "focal point" for the image after selecting it. This ensures that this particular point remains visible even if CSS would otherwise crop it, which is a common issue in responsive design. See the `@apostrophecms/image` widget for a sample implementation of the necessary styles.
9
+ * Adds the `aspectRatio` option for image widgets. When set to `[ w, h ]` (a ratio of width to height), images are automatically cropped to this aspect ratio when chosen for that particular widget. If the user does not crop manually, then cropping happens automatically.
10
+ * Adds the `minSize` option for image widgets. This ensures that the images chosen are at least the given size `[ width, height ]`, and also ensures the user cannot choose something smaller than that when cropping.
11
+ * Implements OpenTelemetry instrumentation.
12
+ * Developers may now specify an alternate Vue component to be used for editing the subfields of relationships, either at the field level or as a default for all relationships with a particular piece type.
13
+ * The widget type base module now always passes on the `components` option as browser data, so that individual widget type modules that support contextual editing can be implemented more conveniently.
14
+ * In-context widget editor components now receive a `focused` prop which is helpful in deciding when to display additional UI.
15
+ * Adds new configuration option - `beforeExit` async handler.
16
+ * Handlers listening for the `apostrophe:run` event are now able to send an exit code to the Apostrophe bootstrap routine.
17
+ * Support for Node.js 17 and 18. MongoDB connections to `localhost` will now successfully find a typical dev MongoDB server bound only to `127.0.0.1`, Apostrophe can generate valid ipv6 URLs pointing back to itself, and `webpack` and `vue-loader` have been updated to address incompatibilities.
18
+ * Adds support for custom context menus provided by any module (see `apos.doc.addContextOperation()`).
19
+ * The `AposSchema` component now supports an optional `generation` prop which may be used to force a refresh when the value of the object changes externally. This is a compromise to avoid the performance hit of checking numerous subfields for possible changes every time the `value` prop changes in response to an `input` event.
20
+ * Adds new event `@apostrophecms/doc:afterAllModesDeleted` fired after all modes of a given document are purged.
21
+
22
+ ### Fixes
23
+
24
+ * Documentation of obsolete options has been removed.
25
+ * Dead code relating to activating in-context widget editors have been removed. They are always active and have been for some time. In the future they might be swapped in on scroll, but there will never be a need to swap them in "on click."
26
+ * The `self.email` method of modules now correctly accepts a default `from` address configured for a specific module via the `from` subproperty of the `email` option to that module. Thanks to `chmdebeer` for pointing out the issue and the fix.
27
+ * Fixes `_urls` not added on attachment fields when pieces API index is requested (#3643)
28
+ * Fixes float field UI bug that transforms the value to integer when there is no field error and the first number after the decimal is `0`.
29
+
30
+ ## 3.17.0 (2022-03-31)
4
31
 
5
32
  ### Adds
6
33
 
@@ -9,13 +36,18 @@
9
36
  * Adds possibility for modules to [extend the webpack configuration](https://v3.docs.apostrophecms.org/guide/webpack.html).
10
37
  * Adds possibility for modules to [add extra frontend bundles for scss and js](https://v3.docs.apostrophecms.org/guide/webpack.html). This is useful when the `ui/src` build would otherwise be very large due to code used on rarely accessed pages.
11
38
  * Loads the right bundles on the right pages depending on the page template and the loaded widgets. Logged-in users have all the bundles on every page, because they might introduce widgets at any time.
39
+ * Fixes deprecation warnings displayed after running `npm install`, for dependencies that are directly included by this package.
40
+ * Implement custom ETags emission when `etags` cache option is enabled. [See the documentation for more information](https://v3.docs.apostrophecms.org/guide/caching.html).
41
+ It allows caching of pages and pieces, using a cache invalidation mechanism that takes into account related (and reverse related) document updates, thanks to backlinks mentioned above.
42
+ Note that for now, only single pages and pieces benefit from the ETags caching system (pages' and pieces' `getOne` REST API route, and regular served pages).
43
+ The cache of an index page corresponding to the type of a piece that was just saved will automatically be invalidated. However, please consider that it won't be effective when a related piece is saved, therefore the cache will automatically be invalidated _after_ the cache lifetime set in `maxAge` cache option.
12
44
 
13
45
  ### Fixes
14
46
 
15
47
  * Apostrophe's webpack build now works properly when developing code that imports module-specific npm dependencies from `ui/src` or `ui/apos` when using `npm link` to develop the module in question.
16
48
  * The `es5: true` option to `@apostrophecms/asset` works again.
17
49
 
18
- # 3.16.1 (2022-03-21)
50
+ ## 3.16.1 (2022-03-21)
19
51
 
20
52
  ### Fixes
21
53
 
package/defaults.js CHANGED
@@ -7,6 +7,7 @@ module.exports = {
7
7
  '@apostrophecms/schema': {},
8
8
  '@apostrophecms/uploadfs': {},
9
9
  '@apostrophecms/asset': {},
10
+ '@apostrophecms/busy': {},
10
11
  '@apostrophecms/launder': {},
11
12
  '@apostrophecms/http': {},
12
13
  '@apostrophecms/db': {},
@@ -50,7 +51,6 @@ module.exports = {
50
51
  '@apostrophecms/file': {},
51
52
  '@apostrophecms/file-tag': {},
52
53
  '@apostrophecms/soft-redirect': {},
53
- '@apostrophecms/submitted-draft': {},
54
- '@apostrophecms/busy': {}
54
+ '@apostrophecms/submitted-draft': {}
55
55
  }
56
56
  };
package/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ // this should be loaded first
2
+ const opentelemetry = require('./lib/opentelemetry');
1
3
  const path = require('path');
2
4
  const _ = require('lodash');
3
5
  const argv = require('boring')({ end: true });
@@ -39,6 +41,20 @@ let defaults = require('./defaults.js');
39
41
  // can be set to a number, which will effectively set the cluster
40
42
  // option to `cluster: { processes: n }`.
41
43
  //
44
+ // `openTelemetryProvider`
45
+ //
46
+ // If set, Apostrophe will register it as a global OpenTelemetry tracer provider.
47
+ // The expected value is an object, an instance of TracerProvider.
48
+ // If the Node SDK is used in the application instead of manual configuration,
49
+ // the provider instance is only available as a
50
+ // private property: `sdkInstance._tracerProvider`. An issue can be opened
51
+ // to discuss the exposure of a public getter with the OpenTelemetry developers.
52
+ //
53
+ // `beforeExit`
54
+ //
55
+ // If set, Apostrophe will invoke it (await) before invoking process.exit.
56
+ // `beforeExit` may be an async function, will be awaited, and takes no arguments.
57
+ //
42
58
  // ## Awaiting the Apostrophe function
43
59
  //
44
60
  // The apos function is async, but in typical cases you do not
@@ -59,8 +75,14 @@ let defaults = require('./defaults.js');
59
75
  // `null` in the primary process. In the child process it resolves as
60
76
  // documented above.
61
77
 
78
+ // The actual entry point, a wrapper that enables the telemetry and starts the
79
+ // root span
62
80
  module.exports = async function(options) {
81
+ const telemetry = opentelemetry(options);
82
+ let spanName = 'apostrophe:boot';
63
83
  const guardTime = 20000;
84
+
85
+ // Detect cluster options
64
86
  if (process.env.APOS_CLUSTER_PROCESSES) {
65
87
  options.cluster = {
66
88
  processes: parseInt(process.env.APOS_CLUSTER_PROCESSES)
@@ -70,48 +92,72 @@ module.exports = async function(options) {
70
92
  console.log('NODE_ENV is not set to production, disabling cluster mode');
71
93
  options.cluster = false;
72
94
  }
95
+
96
+ // Execute if cluster enabled
73
97
  if (options.cluster && !argv._.length) {
74
98
  // For bc with node 14 and below we need to check both
75
99
  if (cluster.isPrimary || cluster.isMaster) {
76
- let processes = options.cluster.processes || cpus().length;
77
- if (processes <= 0) {
78
- processes = cpus().length + processes;
79
- }
80
- let capped = '';
81
- if (processes > cpus().length) {
82
- processes = cpus().length;
83
- capped = ' (capped to number of CPU cores)';
84
- }
85
- if (processes < 2) {
86
- processes = 2;
87
- if (capped) {
88
- capped = ' (less than 2 cores, capped to minimum of 2)';
89
- } else {
90
- capped = ' (using minimum of 2)';
100
+ // Activate and return the callback return value
101
+ return telemetry.startActiveSpan(`${spanName}:primary`, async (span) => {
102
+ let processes = options.cluster.processes || cpus().length;
103
+ if (processes <= 0) {
104
+ processes = cpus().length + processes;
91
105
  }
92
- }
93
- console.log(`Starting ${processes} cluster child processes${capped}`);
94
- for (let i = 0; i < processes; i++) {
95
- clusterFork();
96
- }
97
- cluster.on('exit', (worker, code, signal) => {
98
- if (code !== 0) {
99
- if ((Date.now() - worker.bornAt) < guardTime) {
100
- console.error(`Worker process ${worker.process.pid} failed in ${seconds(Date.now() - worker.bornAt)}, waiting ${seconds(guardTime)} before restart`);
101
- setTimeout(() => {
102
- respawn(worker);
103
- }, guardTime);
106
+ let capped = '';
107
+ if (processes > cpus().length) {
108
+ processes = cpus().length;
109
+ capped = ' (capped to number of CPU cores)';
110
+ }
111
+ if (processes < 2) {
112
+ processes = 2;
113
+ if (capped) {
114
+ capped = ' (less than 2 cores, capped to minimum of 2)';
104
115
  } else {
105
- respawn(worker);
116
+ capped = ' (using minimum of 2)';
117
+ }
118
+ }
119
+ console.log(`Starting ${processes} cluster child processes${capped}`);
120
+ for (let i = 0; i < processes; i++) {
121
+ clusterFork();
122
+ }
123
+ cluster.on('exit', (worker, code, signal) => {
124
+ if (code !== 0) {
125
+ if ((Date.now() - worker.bornAt) < guardTime) {
126
+ console.error(`Worker process ${worker.process.pid} failed in ${seconds(Date.now() - worker.bornAt)}, waiting ${seconds(guardTime)} before restart`);
127
+ setTimeout(() => {
128
+ respawn(worker);
129
+ }, guardTime);
130
+ } else {
131
+ respawn(worker);
132
+ }
106
133
  }
134
+ });
135
+ span.end();
136
+ if (typeof options.beforeExit === 'function') {
137
+ await options.beforeExit();
107
138
  }
139
+ return null;
108
140
  });
109
- return null;
110
141
  } else {
142
+ // continue as a worker operation, the pid should be recorded by the auto instrumentation
143
+ spanName += ':worker';
111
144
  console.log(`Cluster worker ${process.pid} started`);
112
145
  }
113
146
  }
114
147
 
148
+ // Create and activate the root span for the boot tracer
149
+ const self = await telemetry.startActiveSpan(spanName, async (span) => {
150
+ const res = await apostrophe(options, telemetry, span);
151
+ span.setStatus(telemetry.api.SpanStatusCode.OK);
152
+ span.end();
153
+ return res;
154
+ });
155
+
156
+ return self;
157
+ };
158
+
159
+ // The actual apostrophe bootstrap
160
+ async function apostrophe(options, telemetry, rootSpan) {
115
161
  // The core is not a true moog object but it must look enough like one
116
162
  // to participate as an async event emitter
117
163
  const self = {
@@ -120,6 +166,43 @@ module.exports = async function(options) {
120
166
  }
121
167
  };
122
168
 
169
+ // Terminates the process. Emits the `apostrophe:beforeExit` async event;
170
+ // use this mechanism to invoke any pre-exit application level tasks. Any
171
+ // `beforeExit` handler errors will be ignored.
172
+ // Invokes and awaits `options.beforeExit` function if available,
173
+ // passing as arguments the exit code and message (if any).
174
+ self._exit = async function(code = 0, message) {
175
+ try {
176
+ if (self.emit) {
177
+ await self.emit('beforeExit');
178
+ }
179
+ } catch (e) {
180
+ // we are at the point where errors are ignored,
181
+ // if emitter is already registered, all handler errors
182
+ // are already recorded by the event module instrumentation
183
+ console.error('beforeExit emit error', e);
184
+ }
185
+
186
+ if (code !== 0) {
187
+ telemetry.handleError(rootSpan, message);
188
+ } else {
189
+ rootSpan.setStatus({
190
+ code: telemetry.api.SpanStatusCode.OK,
191
+ message
192
+ });
193
+ }
194
+ rootSpan.end();
195
+
196
+ if (typeof options.beforeExit === 'function') {
197
+ try {
198
+ await options.beforeExit(code, message);
199
+ } catch (e) {
200
+ console.error('beforeExit handler error', e);
201
+ }
202
+ }
203
+ process.exit(code);
204
+ };
205
+
123
206
  try {
124
207
  const matches = process.version.match(/^v(\d+)/);
125
208
  const version = parseInt(matches[1]);
@@ -130,6 +213,9 @@ module.exports = async function(options) {
130
213
  // promise event emitter code
131
214
  self.apos = self;
132
215
 
216
+ // Register the telemetry API as a pseudo module
217
+ self.apos.telemetry = telemetry;
218
+
133
219
  Object.assign(self, require('./modules/@apostrophecms/module/lib/events.js')(self));
134
220
 
135
221
  // Determine root module and root directory
@@ -212,17 +298,22 @@ module.exports = async function(options) {
212
298
  await self.apos.page.implementParkAllInOtherLocales();
213
299
  });
214
300
  await self.emit('ready'); // formerly afterInit
301
+
215
302
  if (self.taskRan) {
216
- process.exit(0);
303
+ await self._exit();
217
304
  } else {
218
- await self.emit('run', self.isTask());
305
+ const after = { exit: null };
306
+ await self.emit('run', self.isTask(), after);
307
+ if (after.exit !== null) {
308
+ await self._exit(after.exit);
309
+ }
219
310
  }
311
+
220
312
  return self;
221
313
  } catch (e) {
222
314
  if (options.exit !== false) {
223
- /* eslint-disable-next-line no-console */
224
315
  console.error(e);
225
- process.exit(1);
316
+ await self._exit(1, e);
226
317
  }
227
318
  }
228
319
 
@@ -0,0 +1,8 @@
1
+ module.exports = host => {
2
+ if (host.includes(':')) {
3
+ // ipv6
4
+ return `[${host}]`;
5
+ } else {
6
+ return host;
7
+ }
8
+ };
@@ -0,0 +1,55 @@
1
+ const mongo = require('mongodb');
2
+ const dns = require('dns');
3
+
4
+ // Connect to MongoDB, using the modern topology and parser, and
5
+ // a tolerant policy to successfully connect to "localhost" even if
6
+ // the first record returned by the resolver doesn't reach mongodb's
7
+ // bind address because localhost resolves first to ::1 (ipv6) and
8
+ // mongodb lists only on 127.0.0.1 (ipv4) by default. For broadest
9
+ // compatibility we don't assume we know this will happen, we try all the
10
+ // addresses that localhost actually resolves to and succeed with the
11
+ // first one that works.
12
+
13
+ module.exports = async (uri, options) => {
14
+ const connectOptions = {
15
+ useUnifiedTopology: true,
16
+ useNewUrlParser: true,
17
+ ...options
18
+ };
19
+ const parsed = new URL(uri);
20
+ if ((parsed.protocol !== 'mongodb:') || (parsed.hostname !== 'localhost')) {
21
+ return mongo.MongoClient.connect(parsed.toString(), connectOptions);
22
+ }
23
+ const records = await dns.promises.lookup('localhost', { all: true });
24
+ if (!records.length) {
25
+ // The computer that reaches this point has bigger problems 😅
26
+ throw new Error('Unable to resolve localhost to an IP address.');
27
+ }
28
+ return new Promise((resolve, reject) => {
29
+ let failed = 0;
30
+ let succeeded = false;
31
+ records.forEach(attempt);
32
+ async function attempt(record) {
33
+ try {
34
+ const parsed = new URL(uri);
35
+ parsed.hostname = record.address;
36
+ const result = await mongo.MongoClient.connect(parsed.toString(), connectOptions);
37
+ if (!succeeded) {
38
+ succeeded = true;
39
+ resolve(result);
40
+ } else {
41
+ // We succeeded in reaching localhost at both ip4 and ip6,
42
+ // but we only need one of them to succeed
43
+ await result.close();
44
+ }
45
+ } catch (e) {
46
+ failed++;
47
+ if (failed === records.length) {
48
+ // None succeeded, so reject with the last error
49
+ // (which one we reject with doesn't really matter)
50
+ reject(e);
51
+ }
52
+ }
53
+ }
54
+ });
55
+ };
@@ -0,0 +1,144 @@
1
+ // Initialize OpenTelemetry tracer singleton and export
2
+ // the most useful API members, the tracer and some helper functions.
3
+ //
4
+ // The library shouldn't be used directly (although it's not fatal),
5
+ // it is registered as self.apos.telemetry pseudo module (see index.js).
6
+ const api = require('@opentelemetry/api');
7
+ const util = require('util');
8
+ const version = require('../package.json').version;
9
+
10
+ /** @type {api.Tracer} */
11
+ let tracer;
12
+
13
+ const Attributes = {
14
+ SCENE: 'apos.scene',
15
+ TEMPLATE: 'apos.template',
16
+ EVENT_MODULE: 'apos.event.module',
17
+ EVENT_NAME: 'apos.event.name',
18
+ // The module and method targeted by the current operation (see doc module)
19
+ TARGET_NAMESPACE: 'apos.target.namespace',
20
+ TARGET_FUNCTION: 'apos.target.function',
21
+ ARGV: 'apos.argv'
22
+ };
23
+
24
+ module.exports = function (options = {}) {
25
+ if (!tracer) {
26
+ tracer = api.trace.getTracer('apostrophe', version);
27
+
28
+ // This shouldn't be needed, but having it doesn't hurt
29
+ if (options.openTelemetryProvider) {
30
+ api.trace.setGlobalTracerProvider(options.openTelemetryProvider);
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Start and return a new span. Optionally provide a parent span or allow the parent
36
+ * span to be auto-detected.
37
+ * Use this when the current code block does the tracing and it doesn't expect
38
+ * more tracing to happen down the line.
39
+ *
40
+ * @param {String} name span name
41
+ * @param {api.Span|Boolean} [parentSpan] optional parent span
42
+ * @param {api.SpanOptions} [options] span options
43
+ * @returns {api.Span}
44
+ * @example
45
+ * // Auto-merge with the currently active span context
46
+ * const span = self.apos.telemetry.startSpan('event:someEvent');
47
+ * // Provide a parent span
48
+ * const span = self.apos.telemetry.startSpan('event:someEvent', parent);
49
+ * // Do not merge with any parent, start as a root
50
+ * const span = self.apos.telemetry.startSpan('event:someEvent', false);
51
+ * // Provide options for the newly created span
52
+ * const span = self.apos.telemetry.startSpan('event:someEvent', true, {
53
+ * attributes: [
54
+ * [SemanticAttributes.HTTP_METHOD]: "GET",
55
+ * [SemanticAttributes.HTTP_FLAVOR]: "1.1",
56
+ * [SemanticAttributes.HTTP_URL]: req.url
57
+ * ]
58
+ * });
59
+ */
60
+ function startSpan(name, parentSpan, options) {
61
+ if ((!parentSpan || parentSpan === true) && parentSpan !== false) {
62
+ parentSpan = api.trace.getSpan(api.context.active());
63
+ }
64
+
65
+ if (parentSpan) {
66
+ const ctx = api.trace.setSpan(api.context.active(), parentSpan);
67
+ return tracer.startSpan(name, options || undefined, ctx);
68
+ }
69
+
70
+ return tracer.startSpan(name, options || undefined);
71
+ }
72
+
73
+ /**
74
+ * Start span and make it active for all the nested spans.
75
+ * Use this when the current code block does the tracing, but it also expects
76
+ * more tracing to happen down the line (async calls).
77
+ *
78
+ * @param {String} name span name
79
+ * @param {Function} fn handler function
80
+ * @param {api.Span|Boolean} [parentSpan] optional parent span
81
+ * @param {api.SpanOptions} [options] span options
82
+ * @returns {api.Span|any} the return value of the handler or the newly created span
83
+ * @example
84
+ * // Activate span, return some value
85
+ * const value = await self.apos.telemetry.startActiveSpan(spanName, async (span) => {
86
+ * // Use the span, do work, end span, return any value
87
+ * span.end();
88
+ * return value;
89
+ * });
90
+ * // Activate span, using the context of parent span, return the active span
91
+ * const span = self.apos.telemetry.startActiveSpan(spanName, async (span) => {
92
+ * // Use the span, do work, return the span
93
+ * return span;
94
+ * });
95
+ */
96
+ function startActiveSpan(name, fn, parentSpan, options) {
97
+ if (parentSpan) {
98
+ const ctx = api.trace.setSpan(api.context.active(), parentSpan);
99
+ return tracer.startActiveSpan(name, options || undefined, ctx, fn);
100
+ }
101
+
102
+ return tracer.startActiveSpan(name, options || undefined, fn);
103
+ }
104
+
105
+ /**
106
+ * Handle errors helper (catch block).
107
+ *
108
+ * @param {api.Span} span
109
+ * @param {Error|String} err
110
+ */
111
+ function handleError(span, err) {
112
+ span.recordException(err);
113
+ span.setStatus({
114
+ code: api.SpanStatusCode.ERROR,
115
+ message: err
116
+ ? typeof err === 'string' ? err : err.message
117
+ : undefined
118
+ });
119
+ }
120
+
121
+ /**
122
+ * Stringify object - helper to properly set
123
+ * object as a span attribute value.
124
+ *
125
+ * @param {Object} obj
126
+ * @returns {String}
127
+ */
128
+ function stringify(obj) {
129
+ return util.inspect(obj, {
130
+ depth: 20,
131
+ compact: false
132
+ });
133
+ }
134
+
135
+ return {
136
+ api,
137
+ tracer,
138
+ Attributes,
139
+ stringify,
140
+ handleError,
141
+ startSpan,
142
+ startActiveSpan
143
+ };
144
+ };
@@ -5,6 +5,8 @@ export default function() {
5
5
 
6
6
  let widgetsRendering = 0;
7
7
 
8
+ apos.area.widgetOptions = [];
9
+
8
10
  createWidgetClipboardApp();
9
11
 
10
12
  createAreaApps();
@@ -38,6 +38,7 @@
38
38
  :area-id="areaId"
39
39
  :key="widget._id"
40
40
  :widget="widget"
41
+ :generation="generation"
41
42
  :i="i"
42
43
  :options="options"
43
44
  :next="next"
@@ -115,23 +116,23 @@ export default {
115
116
  default() {
116
117
  return {};
117
118
  }
119
+ },
120
+ generation: {
121
+ type: Number,
122
+ required: false,
123
+ default() {
124
+ return null;
125
+ }
118
126
  }
119
127
  },
120
128
  emits: [ 'changed' ],
121
129
  data() {
122
- const validItems = this.items.filter(item => {
123
- if (!window.apos.modules[`${item.type}-widget`]) {
124
- console.warn(`The widget type ${item.type} exists in the content but is not configured.`);
125
- }
126
- return window.apos.modules[`${item.type}-widget`];
127
- });
128
-
129
130
  return {
130
131
  addWidgetEditor: null,
131
132
  addWidgetOptions: null,
132
133
  addWidgetType: null,
133
134
  areaId: cuid(),
134
- next: validItems,
135
+ next: this.getValidItems(),
135
136
  hoveredWidget: null,
136
137
  focusedWidget: null,
137
138
  contextMenuOptions: {
@@ -189,6 +190,9 @@ export default {
189
190
  _id: this.id,
190
191
  items: this.next
191
192
  });
193
+ },
194
+ generation() {
195
+ this.next = this.getValidItems();
192
196
  }
193
197
  },
194
198
  mounted() {
@@ -506,6 +510,14 @@ export default {
506
510
  } else {
507
511
  return this.renderings[widget._id];
508
512
  }
513
+ },
514
+ getValidItems() {
515
+ return this.items.filter(item => {
516
+ if (!window.apos.modules[`${item.type}-widget`]) {
517
+ console.warn(`The widget type ${item.type} exists in the content but is not configured.`);
518
+ }
519
+ return window.apos.modules[`${item.type}-widget`];
520
+ });
509
521
  }
510
522
  }
511
523
  };
@@ -101,6 +101,8 @@
101
101
  :options="options.widgets[widget.type]"
102
102
  :type="widget.type"
103
103
  :doc-id="docId"
104
+ :focused="focused"
105
+ :key="generation"
104
106
  />
105
107
  <component
106
108
  v-else
@@ -114,6 +116,7 @@
114
116
  @edit="$emit('edit', i);"
115
117
  :doc-id="docId"
116
118
  :rendering="rendering"
119
+ :key="generation"
117
120
  />
118
121
  <div
119
122
  class="apos-area-widget-controls apos-area-widget-controls--add apos-area-widget-controls--add--bottom"
@@ -199,6 +202,13 @@ export default {
199
202
  disabled: {
200
203
  type: Boolean,
201
204
  default: false
205
+ },
206
+ generation: {
207
+ type: Number,
208
+ required: false,
209
+ default() {
210
+ return null;
211
+ }
202
212
  }
203
213
  },
204
214
  emits: [ 'clone', 'up', 'down', 'remove', 'edit', 'cut', 'copy', 'update', 'add', 'changed' ],
@@ -53,6 +53,7 @@ module.exports = {
53
53
  'help-circle-icon': 'HelpCircle',
54
54
  'information-outline-icon': 'InformationOutline',
55
55
  'information-icon': 'Information',
56
+ 'image-edit-outline': 'ImageEditOutline',
56
57
  'image-icon': 'Image',
57
58
  'image-size-select-actual-icon': 'ImageSizeSelectActual',
58
59
  'play-box-icon': 'PlayBox',