apostrophe 3.35.0 → 3.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.37.0 (2023-01-06)
4
+
5
+ ### Adds
6
+
7
+ * Dynamic choice functions in schemas now also receive a data object with their original doc id for further inspection by your function.
8
+ * Use `mergeWithCustomize` when merging extended source Webpack configuration. Introduce overideable asset module methods `srcCustomizeArray` and `srcCustomizeObject`, with reasonable default behavior, for fine tuning Webpack config arrays and objects merging. More info - [the Webpack mergeWithCustomize docs](https://github.com/survivejs/webpack-merge#mergewithcustomize-customizearray-customizeobject-configuration--configuration)
9
+ * The image widget now accepts a `placeholderImage` option that works like `previewImage` (just specify a file extension, like `placeholderImage: 'jpg'`, and provide the file `public/placeholder.jpg` in the module). The `placeholderUrl` option is still available for backwards compatibility.
10
+
11
+ ### Fixes
12
+
13
+ * `docId` is now properly passed through array and object fields and into their child schemas.
14
+ * Remove module `@apostrophecms/polymorphic-type` name alias `@apostrophecms/polymorphic`. It was causing warnings
15
+ e.g. `A permission.can() call was made with a type that has no manager: @apostrophecms/polymorphic-type`.
16
+ * The module `webpack.extensions` configuration is not applied to the core Admin UI build anymore. This is the correct and intended behavior as explained in the [relevant documentation](https://v3.docs.apostrophecms.org/guide/webpack.html#extending-webpack-configuration).
17
+ * The `previewImage` option now works properly for widget modules loaded from npm and those that subclass them. Specifically, the preview image may be provided in the `public/` subdirectory of the original module, the project-level configuration of it, or a subclass.
18
+
19
+ ## 3.36.0 (2022-12-22)
20
+
21
+ ### Adds
22
+
23
+ * `shortcut` option for piece modules, allowing easy re-mapping of the manager command shortcut per module.
24
+
25
+ ### Fixes
26
+
27
+ * Ensure there are no conflicting command shortcuts for the core modules.
3
28
 
4
29
  ## 3.35.0 (2022-12-21)
5
30
 
@@ -363,7 +363,7 @@ export default {
363
363
  apos.area.activeEditor = this;
364
364
  const result = await apos.modal.execute(componentName, {
365
365
  value: widget,
366
- options: this.options.widgets[widget.type],
366
+ options: this.widgetOptionsByType(widget.type),
367
367
  type: widget.type,
368
368
  docId: this.docId
369
369
  });
@@ -36,8 +36,8 @@
36
36
  class="apos-icon--add"
37
37
  />
38
38
  <img
39
- v-if="item.previewImage"
40
- :src="previewUrl(item)"
39
+ v-if="item.previewUrl"
40
+ :src="item.previewUrl"
41
41
  :alt="`${item.name} preview`"
42
42
  class="apos-widget__preview-image"
43
43
  >
@@ -176,9 +176,6 @@ export default {
176
176
  hasIcon(widget) {
177
177
  return widget.previewIcon || widget.icon;
178
178
  },
179
- previewUrl(widget) {
180
- return widget.previewImage ? apos.util.assetUrl(`/modules/${widget.name}-widget/preview.${widget.previewImage}`) : '';
181
- },
182
179
  close() {
183
180
  this.modal.showModal = false;
184
181
  },
@@ -7,7 +7,7 @@ const path = require('path');
7
7
  const util = require('util');
8
8
  const express = require('express');
9
9
  const { stripIndent } = require('common-tags');
10
- const { merge: webpackMerge } = require('webpack-merge');
10
+ const { mergeWithCustomize: webpackMerge } = require('webpack-merge');
11
11
  const cuid = require('cuid');
12
12
  const chokidar = require('chokidar');
13
13
  const _ = require('lodash');
@@ -73,7 +73,6 @@ module.exports = {
73
73
  self.buildWatcherEnable = process.env.APOS_ASSET_WATCH !== '0' && self.options.watch !== false;
74
74
  self.buildWatcherDebounceMs = parseInt(self.options.watchDebounceMs || 1000, 10);
75
75
  self.buildWatcher = null;
76
-
77
76
  },
78
77
  handlers (self) {
79
78
  return {
@@ -389,8 +388,11 @@ module.exports = {
389
388
  es5TaskFn: self.es5TaskFn
390
389
  }, self.apos);
391
390
 
392
- const webpackInstanceConfigMerged = self.webpackExtensions
393
- ? webpackMerge(webpackInstanceConfig, ...Object.values(self.webpackExtensions))
391
+ const webpackInstanceConfigMerged = !options.apos && self.webpackExtensions
392
+ ? webpackMerge({
393
+ customizeArray: self.srcCustomizeArray,
394
+ customizeObject: self.srcCustomizeObject
395
+ })(webpackInstanceConfig, ...Object.values(self.webpackExtensions))
394
396
  : webpackInstanceConfig;
395
397
 
396
398
  // Inject the cache location at the end - we need the merged config
@@ -941,6 +943,28 @@ module.exports = {
941
943
  // Open the implementation for more dev comments.
942
944
  transformRebundledFor,
943
945
 
946
+ // Optional functions passed to webpack's mergeWithCustomize, allowing
947
+ // fine control over merging of the webpack configuration for the
948
+ // src build. Extend or override to alter the default behavior.
949
+ // See https://github.com/survivejs/webpack-merge#mergewithcustomize-customizearray-customizeobject-configuration--configuration
950
+ srcCustomizeArray(a, b, key) {
951
+ // Keep arrays unique when merging
952
+ if (
953
+ [
954
+ 'resolveLoader.extensions',
955
+ 'resolveLoader.modules',
956
+ 'resolve.extensions',
957
+ 'resolve.modules'
958
+ ].includes(key)
959
+ ) {
960
+ return _.uniq([ ...a, ...b ]);
961
+ }
962
+ },
963
+
964
+ srcCustomizeObject(a, b, key) {
965
+ // override to alter the default webpack merge behavior
966
+ },
967
+
944
968
  async initUploadfs() {
945
969
  if (self.options.uploadfs) {
946
970
  self.uploadfs = await self.apos.modules['@apostrophecms/uploadfs'].getInstance(self.options.uploadfs);
@@ -1319,6 +1343,11 @@ module.exports = {
1319
1343
  return 'url(\'' + filter(url) + '\')';
1320
1344
  });
1321
1345
  return css;
1346
+ },
1347
+ // Return the URL of the asset with the given path, taking into account
1348
+ // the release id, uploadfs, etc.
1349
+ url(path) {
1350
+ return `${self.getAssetBaseUrl()}${path}`;
1322
1351
  }
1323
1352
  };
1324
1353
  },
@@ -1336,8 +1365,10 @@ module.exports = {
1336
1365
  }
1337
1366
  return self.apos.template.safe(`<script data-apos-refresh-on-restart="${self.action}/restart-id" src="${self.action}/refresh-on-restart"></script>`);
1338
1367
  },
1368
+ // Return the URL of the release asset with the given path, taking into account
1369
+ // the release id, uploadfs, etc.
1339
1370
  url(path) {
1340
- return `${self.getAssetBaseUrl()}${path}`;
1371
+ return self.url(path);
1341
1372
  }
1342
1373
  };
1343
1374
  },
@@ -1096,8 +1096,9 @@ module.exports = {
1096
1096
  normalizeType(type) {
1097
1097
  if (type === '@apostrophecms/page') {
1098
1098
  // Backwards compatible
1099
- type = '@apostrophecms/any-page-type';
1099
+ return '@apostrophecms/any-page-type';
1100
1100
  }
1101
+
1101
1102
  return type;
1102
1103
  },
1103
1104
  // Given a doc, an _id, or an aposDocId, this method
@@ -98,7 +98,7 @@ module.exports = {
98
98
  action: 'edit',
99
99
  type: self.__meta.name
100
100
  },
101
- shortcut: `G,${self.apos.task.getReq().t(self.options.label).slice(0, 1)}`
101
+ shortcut: self.options.shortcut ?? `G,${self.apos.task.getReq().t(self.options.label).slice(0, 1)}`
102
102
  },
103
103
  [`${self.__meta.name}:create-new`]: {
104
104
  type: 'item',
@@ -6,7 +6,8 @@ module.exports = {
6
6
  quickCreate: false,
7
7
  autopublish: true,
8
8
  editRole: 'editor',
9
- publishRole: 'editor'
9
+ publishRole: 'editor',
10
+ shortcut: 'G,g'
10
11
  },
11
12
  fields: {
12
13
  remove: [ 'visibility' ]
@@ -6,7 +6,8 @@ module.exports = {
6
6
  quickCreate: false,
7
7
  autopublish: true,
8
8
  editRole: 'editor',
9
- publishRole: 'editor'
9
+ publishRole: 'editor',
10
+ shortcut: 'G,o'
10
11
  },
11
12
  fields: {
12
13
  remove: [ 'visibility' ]
@@ -7,7 +7,7 @@ module.exports = {
7
7
  dimensionAttrs: false,
8
8
  placeholder: true,
9
9
  placeholderClass: false,
10
- placeholderUrl: '/modules/@apostrophecms/image-widget/placeholder.jpg'
10
+ placeholderImage: 'jpg'
11
11
  },
12
12
  fields: {
13
13
  add: {
@@ -19,5 +19,8 @@ module.exports = {
19
19
  withType: '@apostrophecms/image'
20
20
  }
21
21
  }
22
+ },
23
+ init(self) {
24
+ self.determineBestAssetUrl('placeholder');
22
25
  }
23
26
  };
@@ -1,6 +1,6 @@
1
1
  {% if data.widget.aposPlaceholder and data.manager.options.placeholderUrl %}
2
2
  <img
3
- src="{{ apos.asset.url(data.manager.options.placeholderUrl) }}"
3
+ src="{{ data.manager.options.placeholderUrl }}"
4
4
  alt="{{ __t('apostrophe:imagePlaceholder') }}"
5
5
  class="image-widget-placeholder"
6
6
  />
@@ -27,6 +27,7 @@
27
27
 
28
28
  const { SemanticAttributes } = require('@opentelemetry/semantic-conventions');
29
29
  const _ = require('lodash');
30
+ const fs = require('fs');
30
31
 
31
32
  module.exports = {
32
33
 
@@ -761,6 +762,46 @@ module.exports = {
761
762
  );
762
763
  },
763
764
 
765
+ // Given a name such as "placeholder", look at the
766
+ // relevant options (nameImage and, as a fallback, nameUrl)
767
+ // and determine the appropriate asset URL. If nameImage is used,
768
+ // search the inheritance chain of the module
769
+ // for the best match, e.g. a file in a project-level override
770
+ // wins, followed by the original module in core or npm,
771
+ // followed by something in a base class that extends, etc.
772
+ // If no file is found, the method returns `undefined`.
773
+ //
774
+ // Even if `nameUrl is used, the method still corrects paths
775
+ // beginning with `/module` to account for the actual asset
776
+ // release URL (`nameUrl` is really an asset path, but for bc
777
+ // this is the naming pattern).
778
+ //
779
+ // In the above examples "name" should be replaced with the
780
+ // actual value of the name argument.
781
+
782
+ determineBestAssetUrl(name) {
783
+ let urlOption = self.options[`${name}Url`];
784
+ const imageOption = self.options[`${name}Image`];
785
+ if (!urlOption) {
786
+ if (imageOption) {
787
+ const chain = [ ...self.__meta.chain ].reverse();
788
+ for (const entry of chain) {
789
+ const path = `${entry.dirname}/public/${name}.${imageOption}`;
790
+ if (fs.existsSync(path)) {
791
+ urlOption = `/modules/${entry.name}/${name}.${imageOption}`;
792
+ break;
793
+ }
794
+ }
795
+ }
796
+ }
797
+ if (urlOption && urlOption.startsWith('/modules')) {
798
+ urlOption = self.apos.asset.url(urlOption);
799
+ }
800
+ if (urlOption) {
801
+ self.options[`${name}Url`] = urlOption;
802
+ }
803
+ },
804
+
764
805
  // Merge in the event emitter / responder capabilities
765
806
  ...require('./lib/events.js')(self)
766
807
  };
@@ -1,11 +1,20 @@
1
1
  const _ = require('lodash');
2
+ const migrations = require('./lib/migrations.js');
2
3
 
3
4
  module.exports = {
4
5
  extend: '@apostrophecms/doc-type',
5
6
  options: {
6
- name: '@apostrophecms/polymorphic',
7
+ name: '@apostrophecms/polymorphic-type',
7
8
  showPermissions: false
8
9
  },
10
+ init(self) {
11
+ self.addMigrations();
12
+ },
13
+ methods(self) {
14
+ return {
15
+ ...migrations(self)
16
+ };
17
+ },
9
18
  routes(self) {
10
19
  return {
11
20
  post: {
@@ -0,0 +1,18 @@
1
+ module.exports = (self) => {
2
+ return {
3
+ addMigrations() {
4
+ self.removePolymorphicTypeAliasMigration();
5
+ },
6
+ removePolymorphicTypeAliasMigration() {
7
+ self.apos.migration.add('remove-polymorphic-type-alias', () => {
8
+ return self.apos.doc.db.updateMany({
9
+ type: '@apostrophecms/polymorphic'
10
+ }, {
11
+ $set: {
12
+ type: '@apostrophecms/polymorphic-type'
13
+ }
14
+ });
15
+ });
16
+ }
17
+ };
18
+ };
@@ -1474,8 +1474,9 @@ module.exports = {
1474
1474
  return {
1475
1475
  get: {
1476
1476
  async choices(req) {
1477
- const id = self.apos.launder.string(req.query.fieldId);
1478
- const field = self.getFieldById(id);
1477
+ const fieldId = self.apos.launder.string(req.query.fieldId);
1478
+ const docId = self.apos.launder.string(req.query.docId);
1479
+ const field = self.getFieldById(fieldId);
1479
1480
  let choices = [];
1480
1481
  if (
1481
1482
  !field ||
@@ -1484,7 +1485,7 @@ module.exports = {
1484
1485
  ) {
1485
1486
  throw self.apos.error('invalid');
1486
1487
  }
1487
- choices = await self.apos.modules[field.moduleName][field.choices](req);
1488
+ choices = await self.apos.modules[field.moduleName][field.choices](req, { docId });
1488
1489
  if (Array.isArray(choices)) {
1489
1490
  return {
1490
1491
  choices
@@ -72,6 +72,7 @@
72
72
  @validate="triggerValidate"
73
73
  :server-errors="currentDocServerErrors"
74
74
  ref="schema"
75
+ :doc-id="docId"
75
76
  />
76
77
  </div>
77
78
  </div>
@@ -109,6 +110,10 @@ export default {
109
110
  serverError: {
110
111
  type: Object,
111
112
  default: null
113
+ },
114
+ docId: {
115
+ type: String,
116
+ default: null
112
117
  }
113
118
  },
114
119
  emits: [ 'modal-result', 'safe-close' ],
@@ -64,6 +64,7 @@
64
64
  :utility-rail="false"
65
65
  :generation="generation"
66
66
  :modifiers="['small', 'inverted']"
67
+ :doc-id="docId"
67
68
  />
68
69
  </div>
69
70
  </transition>
@@ -214,7 +215,8 @@ export default {
214
215
  const result = await apos.modal.execute('AposArrayEditor', {
215
216
  field: this.field,
216
217
  items: this.next,
217
- serverError: this.serverError
218
+ serverError: this.serverError,
219
+ docId: this.docId
218
220
  });
219
221
  if (result) {
220
222
  this.next = result;
@@ -15,6 +15,7 @@
15
15
  :trigger-validation="triggerValidation"
16
16
  :utility-rail="false"
17
17
  :generation="generation"
18
+ :doc-id="docId"
18
19
  v-model="schemaInput"
19
20
  ref="schema"
20
21
  />
@@ -37,6 +38,13 @@ export default {
37
38
  default() {
38
39
  return null;
39
40
  }
41
+ },
42
+ docId: {
43
+ type: String,
44
+ required: false,
45
+ default() {
46
+ return null;
47
+ }
40
48
  }
41
49
  },
42
50
  data () {
@@ -17,7 +17,8 @@ export default {
17
17
  `${action}/choices`,
18
18
  {
19
19
  qs: {
20
- fieldId: this.field._id
20
+ fieldId: this.field._id,
21
+ docId: this.docId
21
22
  },
22
23
  busy: true
23
24
  }
@@ -106,6 +106,8 @@ module.exports = {
106
106
 
107
107
  self.enableBrowserData();
108
108
 
109
+ self.determineBestAssetUrl('preview');
110
+
109
111
  self.template = self.options.template || 'widget';
110
112
 
111
113
  self.name = self.__meta.name.replace(/-widget$/, '');
@@ -362,6 +364,7 @@ module.exports = {
362
364
  version. The method in 3.x simply returns an empty array.`);
363
365
  return [];
364
366
  }
367
+
365
368
  };
366
369
  },
367
370
  extendMethods(self) {
@@ -384,7 +387,7 @@ module.exports = {
384
387
  description: self.options.description,
385
388
  icon: self.options.icon,
386
389
  previewIcon: self.options.previewIcon,
387
- previewImage: self.options.previewImage,
390
+ previewUrl: self.options.previewUrl,
388
391
  action: self.action,
389
392
  schema: schema,
390
393
  contextual: self.options.contextual,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apostrophe",
3
- "version": "3.35.0",
3
+ "version": "3.37.0",
4
4
  "description": "The Apostrophe Content Management System.",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/test/widgets.js CHANGED
@@ -290,7 +290,6 @@ describe('Widgets', function() {
290
290
  },
291
291
  assertFalsyPlaceholderUrl(document) {
292
292
  const imgNodes = document.querySelectorAll('img');
293
-
294
293
  assert(imgNodes.length === 0);
295
294
  },
296
295
  assertPlaceholderUrlOverride(document) {
@@ -409,7 +408,8 @@ describe('Widgets', function() {
409
408
  'placeholder-page': {},
410
409
  [`@apostrophecms/${type}-widget`]: {
411
410
  options: {
412
- placeholderUrl: null
411
+ placeholderUrl: null,
412
+ placeholderImage: null
413
413
  }
414
414
  }
415
415
  }