apostrophe 3.9.0 → 3.10.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,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.10.0 - 2021-12-22
4
+
5
+ ### Fixes
6
+
7
+ * `slug` type fields can now have an empty string or `null` as their `def` value without the string `'none'` populating automatically.
8
+ * The `underline` feature works properly in tiptap toolbar configuration.
9
+ * Required checkbox fields now properly prevent editor submission when empty.
10
+ * Pins `vue-click-outside-element` to a version that does not attempt to use `eval` in its distribution build, which is incompatible with a strict Content Security Policy.
11
+
12
+ ### Adds
13
+
14
+ * Adds a `last` option to fields. Setting `last: true` on a field puts that field at the end of the field's widget order. If more than one field has that option active the true last item will depend on general field registration order. If the field is ordered with the `fields.order` array or field group ordering, those specified orders will take precedence.
15
+
16
+ ### Changes
17
+
18
+ * Adds deprecation notes to the widget class methods `getWidgetWrapperClasses` and `getWidgetClasses` from A2.
19
+ * Adds a deprecation note to the `reorganize` query builder for the next major version.
20
+ * Uses the runtime build of Vue. This has major performance and bundle size benefits, however it does require changes to Apostrophe admin UI apps that use a `template` property (components should require no changes, just apps require an update). These apps must now use a `render` function instead. Since custom admin UI apps are not yet a documented feature we do not regard this as a bc break.
21
+ * Compatible with the `@apostrophecms/security-headers` module, which supports a strict `Content-Security-Policy`.
22
+ * Adds a deprecation note to the `addLateCriteria` query builder.
23
+ * Updates the `toCount` doc type query method to use Math.ceil rather than Math.floor plus an additional step.
24
+
3
25
  ## 3.9.0 - 2021-12-08
4
26
 
5
27
  ### Adds
package/lib/moog.js CHANGED
@@ -179,17 +179,42 @@ module.exports = function(options) {
179
179
  ...properties.add
180
180
  };
181
181
  }
182
+
183
+ const lastFields = Object.entries(that[cascade])
184
+ .filter(([ field, val ]) => val.last === true)
185
+ .map(([ field, val ]) => field);
186
+
182
187
  if (properties.remove) {
183
188
  for (const field of properties.remove) {
184
189
  delete that[cascade][field];
185
190
  }
186
191
  }
187
192
  if (properties.order) {
193
+ // 1. Ordered fields
194
+ // 2. Other fields not marked as last
195
+ // 3. Fields marked as last
188
196
  that[cascade] = Object.fromEntries([
189
197
  ...properties.order.map(field => [ field, that[cascade][field] ]),
190
- ...Object.keys(that[cascade]).filter(field => !properties.order.includes[field]).map(field => [ field, that[cascade][field] ])
198
+ ...Object.keys(that[cascade])
199
+ .filter(field => {
200
+ return !properties.order.includes(field) &&
201
+ !lastFields.includes(field);
202
+ })
203
+ .map(field => [ field, that[cascade][field] ]),
204
+ ...lastFields.filter(field => !properties.order.includes(field))
205
+ .map(field => [ field, that[cascade][field] ])
206
+ ]);
207
+ } else if (lastFields.length > 0) {
208
+ // 1. Fields not marked as last
209
+ // 2. Fields marked as last
210
+ that[cascade] = Object.fromEntries([
211
+ ...Object.keys(that[cascade])
212
+ .filter(field => !lastFields.includes(field))
213
+ .map(field => [ field, that[cascade][field] ]),
214
+ ...lastFields.map(field => [ field, that[cascade][field] ])
191
215
  ]);
192
216
  }
217
+
193
218
  if (properties.group) {
194
219
  const groups = klona(that[`${cascade}Groups`]);
195
220
  for (const value of Object.values(properties.group)) {
@@ -10,7 +10,13 @@ export default function() {
10
10
  return window.apos;
11
11
  }
12
12
  },
13
- template: '<component :is="`TheAposAdminBar`" :items="apos.adminBar.items" />'
13
+ render: function (h) {
14
+ return h('TheAposAdminBar', {
15
+ props: {
16
+ items: apos.adminBar.items
17
+ }
18
+ });
19
+ }
14
20
  });
15
21
  }
16
22
  };
@@ -287,6 +287,8 @@ module.exports = {
287
287
  // are suitable for display in the reorganize view.
288
288
  // The only pages excluded are those with a `reorganize`
289
289
  // property explicitly set to `false`.
290
+ // NOTE: This query builder is deprecated and will be removed in the
291
+ // next major version.
290
292
  reorganize: {
291
293
  def: null,
292
294
  finalize() {
@@ -111,7 +111,19 @@ export default function() {
111
111
  renderings
112
112
  };
113
113
  },
114
- template: `<${component} :options="options" :items="items" :choices="choices" :id="$data.id" :docId="$data.docId" :fieldId="fieldId" :renderings="renderings" />`
114
+ render(h) {
115
+ return h(component, {
116
+ props: {
117
+ options: this.options,
118
+ items: this.items,
119
+ choices: this.choices,
120
+ id: this.id,
121
+ docId: this.docId,
122
+ fieldId: this.fieldId,
123
+ renderings: this.renderings
124
+ }
125
+ });
126
+ }
115
127
  });
116
128
  }
117
129
  }
@@ -752,8 +752,7 @@ module.exports = {
752
752
  if (!self.shouldRefreshOnRestart()) {
753
753
  return '';
754
754
  }
755
- const script = fs.readFileSync(path.join(__dirname, '/lib/refresh-on-restart.js'), 'utf8');
756
- return self.apos.template.safe(`<script data-apos-refresh-on-restart="${self.action}/restart-id">\n${script}</script>`);
755
+ return self.apos.template.safe(`<script data-apos-refresh-on-restart="${self.action}/restart-id" src="${self.action}/refresh-on-restart"></script>`);
757
756
  },
758
757
  url(path) {
759
758
  return `${self.getAssetBaseUrl()}${path}`;
@@ -765,6 +764,12 @@ module.exports = {
765
764
  return;
766
765
  }
767
766
  return {
767
+ get: {
768
+ refreshOnRestart(req) {
769
+ req.res.setHeader('content-type', 'text/javascript');
770
+ return fs.readFileSync(path.join(__dirname, '/lib/refresh-on-restart.js'), 'utf8');
771
+ }
772
+ },
768
773
  // Use a POST route so IE11 doesn't cache it
769
774
  post: {
770
775
  async restartId(req) {
@@ -28,7 +28,7 @@ module.exports = ({
28
28
  optimization: {
29
29
  minimize: process.env.NODE_ENV === 'production'
30
30
  },
31
- devtool: 'eval-source-map',
31
+ devtool: 'source-map',
32
32
  output: {
33
33
  path: outputPath,
34
34
  filename: outputFilename
@@ -42,7 +42,7 @@ module.exports = ({
42
42
  resolve: {
43
43
  extensions: [ '*', '.js', '.vue', '.json' ],
44
44
  alias: {
45
- vue$: 'vue/dist/vue.esm.js',
45
+ vue$: 'vue/dist/vue.runtime.esm.js',
46
46
  // resolve apostrophe modules
47
47
  Modules: path.resolve(modulesDir)
48
48
  },
@@ -34,7 +34,7 @@ module.exports = ({
34
34
  optimization: {
35
35
  minimize: process.env.NODE_ENV === 'production'
36
36
  },
37
- devtool: 'eval-source-map',
37
+ devtool: 'source-map',
38
38
  output: {
39
39
  path: outputPath,
40
40
  filename: outputFilename
@@ -3,6 +3,8 @@ import Vue from 'Modules/@apostrophecms/ui/lib/vue';
3
3
  export default function() {
4
4
  return new Vue({
5
5
  el: '#apos-busy',
6
- template: '<component :is="`TheAposBusy`" />'
6
+ render: function (h) {
7
+ return h('TheAposBusy');
8
+ }
7
9
  });
8
10
  };
@@ -1152,8 +1152,10 @@ module.exports = {
1152
1152
 
1153
1153
  // `.addLateCriteria({...})` provides an object to be merged directly into the final
1154
1154
  // criteria object that will go to MongoDB. This is to be used only
1155
- // in cases where MongoDB forbids the use of an operator inside
1156
- // `$and`, such as the `$near` operator.
1155
+ // in cases where MongoDB forbids the use of an operator inside `$and`.
1156
+ //
1157
+ // TODO: Since `$near` can now be used in `$and` operators, this query
1158
+ // builder is deprecated and should be removed in the 4.x major version.
1157
1159
  addLateCriteria: {
1158
1160
  set(c) {
1159
1161
  let lateCriteria = query.get('lateCriteria');
@@ -1420,7 +1422,7 @@ module.exports = {
1420
1422
  }
1421
1423
  },
1422
1424
 
1423
- // `.permission('admin')` would limit the returned docs to those for which the
1425
+ // `.permission('edit')` would limit the returned docs to those for which the
1424
1426
  // user associated with the query's `req` has the named permission.
1425
1427
  // By default, `view` is checked for. You might want to specify
1426
1428
  // `edit`.
@@ -1658,7 +1660,11 @@ module.exports = {
1658
1660
  const _req = query.req.clone({
1659
1661
  mode: 'published'
1660
1662
  });
1661
- const publishedDocs = await self.find(_req)._ids(results.map(result => result._id.replace(':draft', ':published'))).project(query.get('project')).toArray();
1663
+ const publishedDocs = await self.find(_req)
1664
+ ._ids(results.map(result => {
1665
+ return result._id.replace(':draft', ':published');
1666
+ })).project(query.get('project')).toArray();
1667
+
1662
1668
  for (const doc of results) {
1663
1669
  const publishedDoc = publishedDocs.find(publishedDoc => doc.aposDocId === publishedDoc.aposDocId);
1664
1670
  doc._publishedDoc = publishedDoc;
@@ -2148,10 +2154,8 @@ module.exports = {
2148
2154
  const count = await mongo.count();
2149
2155
  if (query.get('perPage')) {
2150
2156
  const perPage = query.get('perPage');
2151
- let totalPages = Math.floor(count / perPage);
2152
- if (count % perPage) {
2153
- totalPages++;
2154
- }
2157
+ const totalPages = Math.ceil(count / perPage);
2158
+
2155
2159
  query.set('totalPages', totalPages);
2156
2160
  }
2157
2161
  return count;
@@ -5,8 +5,9 @@ export default function() {
5
5
  if (el) {
6
6
  return new Vue({
7
7
  el,
8
- // TODO check apos.login.browser.components.theAposLogin for alternate name
9
- template: '<component :is="`TheAposLogin`" />'
8
+ render: function (h) {
9
+ return h('TheAposLogin');
10
+ }
10
11
  });
11
12
  }
12
13
  apos.bus.$on('admin-menu-click', async (item) => {
@@ -27,11 +27,14 @@ export default function() {
27
27
  return this.$refs.modals.execute(componentName, props);
28
28
  }
29
29
  },
30
- template: `<component
31
- ref="modals"
32
- :is="apos.modal.components.the"
33
- :modals="apos.modal.modals"
34
- />`
30
+ render(h) {
31
+ return h(apos.modal.components.the, {
32
+ ref: 'modals',
33
+ props: {
34
+ modals: apos.modal.modals
35
+ }
36
+ });
37
+ }
35
38
  });
36
39
  apos.modal.execute = theAposModals.execute;
37
40
  apos.confirm = theAposModals.confirm;
@@ -9,6 +9,8 @@ export default function() {
9
9
 
10
10
  return new Vue({
11
11
  el: '#apos-notification',
12
- template: '<TheAposNotifications />'
12
+ render: function (h) {
13
+ return h('TheAposNotifications');
14
+ }
13
15
  });
14
16
  };
@@ -250,7 +250,8 @@ module.exports = {
250
250
  codeBlock: [
251
251
  'pre',
252
252
  'code'
253
- ]
253
+ ],
254
+ underline: [ 'u' ]
254
255
  };
255
256
  for (const item of options.toolbar || []) {
256
257
  if (simple[item]) {
@@ -44,6 +44,7 @@ import StarterKit from '@tiptap/starter-kit';
44
44
  import TextAlign from '@tiptap/extension-text-align';
45
45
  import Highlight from '@tiptap/extension-highlight';
46
46
  import TextStyle from '@tiptap/extension-text-style';
47
+ import Underline from '@tiptap/extension-underline';
47
48
  export default {
48
49
  name: 'AposRichTextWidgetEditor',
49
50
  components: {
@@ -181,7 +182,8 @@ export default {
181
182
  types: [ 'heading', 'paragraph' ]
182
183
  }),
183
184
  Highlight,
184
- TextStyle
185
+ TextStyle,
186
+ Underline
185
187
  ].concat(this.aposTiptapExtensions)
186
188
  });
187
189
  },
@@ -142,11 +142,14 @@ module.exports = {
142
142
  // leading slash required). Otherwise, expect a object-style slug
143
143
  // (no slashes at all)
144
144
  convert: function (req, field, data, destination) {
145
- const options = {};
145
+ const options = {
146
+ def: field.def
147
+ };
146
148
  if (field.page) {
147
149
  options.allow = '/';
148
150
  }
149
151
  destination[field.name] = self.apos.util.slugify(self.apos.launder.string(data[field.name], field.def), options);
152
+
150
153
  if (field.page) {
151
154
  if (!(destination[field.name].charAt(0) === '/')) {
152
155
  destination[field.name] = '/' + destination[field.name];
@@ -1217,12 +1220,14 @@ module.exports = {
1217
1220
 
1218
1221
  // all fields in the schema will end up in this variable
1219
1222
  let newSchema = [];
1223
+
1220
1224
  // loop over any groups and orders we want to respect
1221
1225
  _.each(groups, function (group) {
1222
1226
 
1223
1227
  _.each(group.fields, function (field) {
1224
1228
  // find the field we are ordering
1225
1229
  let f = _.find(schema, { name: field });
1230
+
1226
1231
  if (!f) {
1227
1232
  // May have already been migrated due to subclasses re-grouping fields
1228
1233
  f = _.find(newSchema, { name: field });
@@ -1242,6 +1247,7 @@ module.exports = {
1242
1247
  if (fIndex !== -1) {
1243
1248
  newSchema.splice(fIndex, 1);
1244
1249
  }
1250
+
1245
1251
  newSchema.push(f);
1246
1252
 
1247
1253
  // remove the field from the old schema, if that is where we got it from
@@ -33,12 +33,12 @@ export default {
33
33
  return uid + value.replace(/\s/g, '');
34
34
  },
35
35
  validate(values) {
36
- if (!Array.isArray(this.field.choices)) {
36
+ // The choices and values should always be arrays.
37
+ if (!Array.isArray(this.field.choices) || !Array.isArray(values)) {
37
38
  return 'malformed';
38
39
  }
39
40
 
40
- if (this.field.required &&
41
- !Array.isArray(values) && (!values || !values.length)) {
41
+ if (this.field.required && !values.length) {
42
42
  return 'required';
43
43
  }
44
44
 
@@ -112,6 +112,7 @@
112
112
  // you are debugging a change and need to test all of the different ways a widget has
113
113
  // been used, or are wondering if you can safely remove one.
114
114
 
115
+ const { stripIndent } = require('common-tags');
115
116
  const _ = require('lodash');
116
117
 
117
118
  module.exports = {
@@ -310,12 +311,20 @@ module.exports = {
310
311
  },
311
312
 
312
313
  // override to add CSS classes to the outer wrapper div of the widget.
314
+ // TODO: Remove in the 4.x major version.
313
315
  getWidgetWrapperClasses(widget) {
316
+ self.apos.util.warnDev(stripIndent`
317
+ The getWidgetWrapperClasses method is deprecated and will be removed in the next
318
+ major version. The method in 3.x simply returns an empty array.`);
314
319
  return [];
315
320
  },
316
321
 
317
322
  // Override to add CSS classes to the div of the widget itself.
323
+ // TODO: Remove in the 4.x major version.
318
324
  getWidgetClasses(widget) {
325
+ self.apos.util.warnDev(stripIndent`
326
+ The getWidgetClasses method is deprecated and will be removed in the next major
327
+ version. The method in 3.x simply returns an empty array.`);
319
328
  return [];
320
329
  }
321
330
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apostrophe",
3
- "version": "3.9.0",
3
+ "version": "3.10.0",
4
4
  "description": "The Apostrophe Content Management System.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -34,7 +34,7 @@
34
34
  "@tiptap/extension-link": "^2.0.0-beta.17",
35
35
  "@tiptap/extension-text-align": "^2.0.0-beta.17",
36
36
  "@tiptap/extension-text-style": "^2.0.0-beta.13",
37
- "@tiptap/extension-underline": "^2.0.0-beta.14",
37
+ "@tiptap/extension-underline": "^2.0.0-beta.22",
38
38
  "@tiptap/starter-kit": "^2.0.0-beta.75",
39
39
  "@tiptap/vue-2": "^2.0.0-beta.34",
40
40
  "autoprefixer": "^10.2.4",
@@ -110,7 +110,7 @@
110
110
  "uploadfs": "^1.17.1",
111
111
  "v-tooltip": "^2.0.3",
112
112
  "vue": "^2.6.14",
113
- "vue-click-outside-element": "^1.0.13",
113
+ "vue-click-outside-element": "1.0.13",
114
114
  "vue-loader": "^15.9.6",
115
115
  "vue-material-design-icons": "~4.12.1",
116
116
  "vue-style-loader": "^4.1.2",
package/test/moog.js CHANGED
@@ -284,6 +284,54 @@ describe('moog', function() {
284
284
  assert(myObject.fieldsGroups.basics.fields.includes('five'));
285
285
  });
286
286
 
287
+ it('should order fields with the last option unless the order array overrides', async function() {
288
+ const moog = require('../lib/moog.js')({});
289
+
290
+ moog.define('unorderedObject', {
291
+ cascades: [ 'fields' ],
292
+ fields: {
293
+ add: {
294
+ first: { type: 'string' },
295
+ last: {
296
+ type: 'string',
297
+ last: true
298
+ },
299
+ second: { type: 'string' },
300
+ third: { type: 'string' }
301
+ }
302
+ }
303
+ });
304
+
305
+ moog.define('orderedObject', {
306
+ cascades: [ 'fields' ],
307
+ fields: {
308
+ add: {
309
+ first: { type: 'string' },
310
+ last: {
311
+ type: 'string',
312
+ last: true
313
+ },
314
+ second: { type: 'string' },
315
+ third: { type: 'string' }
316
+ },
317
+ order: [ 'last', 'third', 'second', 'first' ]
318
+ }
319
+ });
320
+ const unordered = await moog.create('unorderedObject', {});
321
+ assert(unordered);
322
+ assert(Object.keys(unordered.fields)[0] === 'first');
323
+ assert(Object.keys(unordered.fields)[1] === 'second');
324
+ assert(Object.keys(unordered.fields)[2] === 'third');
325
+ assert(Object.keys(unordered.fields)[3] === 'last');
326
+
327
+ const ordered = await moog.create('orderedObject', {});
328
+ assert(ordered);
329
+ assert(Object.keys(ordered.fields)[0] === 'last');
330
+ assert(Object.keys(ordered.fields)[1] === 'third');
331
+ assert(Object.keys(ordered.fields)[2] === 'second');
332
+ assert(Object.keys(ordered.fields)[3] === 'first');
333
+ });
334
+
287
335
  // ==================================================
288
336
  // `redefine` AND `isDefined`
289
337
  // ==================================================