apostrophe 3.45.1 → 3.47.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.
@@ -20,8 +20,8 @@ jobs:
20
20
  runs-on: ubuntu-latest
21
21
  strategy:
22
22
  matrix:
23
- node-version: [14, 16, 18]
24
- mongodb-version: [4.2, 5.0]
23
+ node-version: [16, 18]
24
+ mongodb-version: [4.4, 5.0, 6.0]
25
25
 
26
26
  # Steps represent a sequence of tasks that will be executed as part of the job
27
27
  steps:
package/CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.47.0 (2023-05-05)
4
+
5
+ ### Changes
6
+
7
+ * Since Node 14 and MongoDB 4.2 have reached their own end-of-support dates,
8
+ we are **no longer supporting them for A3.** Note that our dependency on
9
+ `jsdom` 22 is incompatible with Node 14. Node 16 and Node 18 are both
10
+ still supported. However, because Node 16 reaches its
11
+ end-of-life date quite soon (September), testing and upgrading directly
12
+ to Node 18 is strongly recommended.
13
+ * Updated `sluggo` to version 1.0.0.
14
+ * Updated `jsdom` to version `22.0.0` to address an installation warning about the `word-wrap` module.
15
+
16
+ ### Fixes
17
+
18
+ * Fix `extendQueries` to use super pattern for every function in builders and methods (and override properties that are not functions).
19
+
20
+ ## 3.46.0 (2023-05-03)
21
+
22
+ ### Fixes
23
+
24
+ * Adding or editing a piece no longer immediately refreshes the main content area if a widget editor is open. This prevents interruption of the widget editing process
25
+ when working with the `@apostrophecms/ai-helper` module, and also helps in other situations.
26
+ * Check that `e.doc` exists when handling `content-changed` event.
27
+ * Require updated `uploadfs` version with no dependency warnings.
28
+
29
+ ### Adds
30
+
31
+ * Allow sub-schema fields (array and object) to follow parent schema fields using the newly introduced `following: '<parentField'` syntax, where the starting `<` indicates the parent level. For example `<parentField` follows a field in the parent level, `<<grandParentField` follows a field in the grandparent level, etc. The change is fully backward compatible with the current syntax for following fields from the same schema level.
32
+
33
+ ### Changes
34
+
35
+ * Debounce search to prevent calling search on every key stroke in the manager modal.
36
+ * Various size and spacing adjustments in the expanded Add Content modal UI
37
+
3
38
  ## 3.45.1 (2023-04-28)
4
39
 
5
40
  ### Fixes
@@ -7,6 +42,7 @@
7
42
  * Added missing styles to ensure consistent presentation of the rich text insert menu.
8
43
  * Fixed a bug in which clicking on an image in the media manager would close the "insert
9
44
  image" dialog box.
45
+ * Update `html-to-text` package to the latest major version.
10
46
 
11
47
  ## 3.45.0 (2023-04-27)
12
48
 
@@ -39,7 +75,11 @@ shouldn't close the link dialog etc.
39
75
 
40
76
  ### Fixes
41
77
 
78
+ * Fix various issues on conditional fields that were occurring when adding new widgets with default values or selecting a falsy value in a field that has a conditional field relying on it.
79
+ Populate new or existing doc instances with default values and add an empty `null` choice to select fields that do not have a default value (required or not) and to the ones configured with dynamic choices.
42
80
  * Rich text widgets save more reliably when many actions are taken quickly just before save.
81
+ * Fix an issue in the `oembed` field where the value was kept in memory after cancelling the widget editor, which resulted in saving the value if the widget was nested and the parent widget was saved.
82
+ Also improve the `oembed` field UX by setting the input as `readonly` rather than `disabled` when fetching the video metadata, in order to avoid losing its focus when typing.
43
83
 
44
84
  ## 3.44.0 (2023-04-13)
45
85
 
@@ -59,11 +99,6 @@ those writing mocha tests of Apostrophe modules.
59
99
  ### Fixes
60
100
  * Fix child page slug when title is deleted
61
101
 
62
- ### Fixes
63
-
64
- * Fix various issues on conditional fields that were occurring when adding new widgets with default values or selecting a falsy value in a field that has a conditional field relying on it.
65
- Populate new or existing doc instances with default values and add an empty `null` choice to select fields that do not have a default value (required or not) and to the ones configured with dynamic choices.
66
-
67
102
  ## 3.43.0 (2023-03-29)
68
103
 
69
104
  ### Adds
@@ -363,6 +363,7 @@ export default {
363
363
  if (!this.widgetIsContextual(widget.type)) {
364
364
  const componentName = this.widgetEditorComponent(widget.type);
365
365
  apos.area.activeEditor = this;
366
+ apos.bus.$on('apos-refreshing', cancelRefresh);
366
367
  const result = await apos.modal.execute(componentName, {
367
368
  value: widget,
368
369
  options: this.widgetOptionsByType(widget.type),
@@ -370,6 +371,7 @@ export default {
370
371
  docId: this.docId
371
372
  });
372
373
  apos.area.activeEditor = null;
374
+ apos.bus.$off('apos-refreshing', cancelRefresh);
373
375
  if (result) {
374
376
  return this.update(result);
375
377
  }
@@ -593,6 +595,9 @@ export default {
593
595
  }
594
596
  };
595
597
 
598
+ function cancelRefresh(refreshOptions) {
599
+ refreshOptions.refresh = false;
600
+ }
596
601
  </script>
597
602
 
598
603
  <style lang="scss" scoped>
@@ -198,7 +198,7 @@ export default {
198
198
 
199
199
  .apos-widget-group {
200
200
  &:not(:last-of-type) {
201
- margin-bottom: 30px;
201
+ margin-bottom: 20px;
202
202
  }
203
203
 
204
204
  .apos-widget__preview {
@@ -206,8 +206,9 @@ export default {
206
206
  display: flex;
207
207
  justify-content: center;
208
208
  align-items: center;
209
+ overflow: hidden;
209
210
  height: 135px;
210
- border: 1px solid var(--a-base-7);
211
+ outline: 1px solid var(--a-base-7);
211
212
  border-radius: var(--a-border-radius);
212
213
  background-color: var(--a-base-10);
213
214
  }
@@ -221,7 +222,7 @@ export default {
221
222
  &--3-columns {
222
223
  display: grid;
223
224
  grid-template-columns: repeat(3, 1fr);
224
- gap: 10px;
225
+ gap: 15px 10px;
225
226
  .apos-widget__preview {
226
227
  height: 89px;
227
228
  }
@@ -230,7 +231,7 @@ export default {
230
231
  &--4-columns {
231
232
  display: grid;
232
233
  grid-template-columns: repeat(4, 1fr);
233
- gap: 5px;
234
+ gap: 15px 5px;
234
235
  .apos-widget__preview {
235
236
  height: 66px;
236
237
  }
@@ -289,8 +290,18 @@ export default {
289
290
  .apos-widget-group__label,
290
291
  .apos-widget__help {
291
292
  @include type-base;
293
+ margin-top: 0;
292
294
  line-height: var(--a-line-tall);
293
- color: var(--a-base-4);
294
295
  text-align: left;
295
296
  }
297
+
298
+ .apos-widget__help {
299
+ color: var(--a-base-2);
300
+ font-size: var(--a-type-smaller);
301
+ }
302
+
303
+ .apos-widget__label {
304
+ line-height: 1.2;
305
+ margin-bottom: 5px;
306
+ }
296
307
  </style>
@@ -1,6 +1,7 @@
1
1
  const { SemanticAttributes } = require('@opentelemetry/semantic-conventions');
2
2
  const _ = require('lodash');
3
3
  const util = require('util');
4
+ const extendQueries = require('./lib/extendQueries');
4
5
 
5
6
  module.exports = {
6
7
  options: {
@@ -472,8 +473,9 @@ module.exports = {
472
473
  }
473
474
  }
474
475
  if (self.extendQueries[name]) {
475
- wrap(query.builders, self.extendQueries[name].builders || {});
476
- wrap(query.methods, self.extendQueries[name].methods || {});
476
+ const extendedQueries = self.extendQueries[name](self, query);
477
+ extendQueries(query.builders, extendedQueries.builders || {});
478
+ extendQueries(query.methods, extendedQueries.methods || {});
477
479
  }
478
480
  }
479
481
  Object.assign(query, query.methods);
@@ -2322,8 +2324,14 @@ module.exports = {
2322
2324
  query.and({ $or });
2323
2325
  }
2324
2326
  }
2325
- }
2327
+ },
2326
2328
 
2329
+ viewContext: {
2330
+ def: null,
2331
+ launder(viewContext) {
2332
+ return [ 'manage', 'relationship' ].includes(viewContext) ? viewContext : null;
2333
+ }
2334
+ }
2327
2335
  },
2328
2336
 
2329
2337
  methods: {
@@ -3073,17 +3081,3 @@ module.exports = {
3073
3081
  };
3074
3082
  }
3075
3083
  };
3076
-
3077
- function wrap(context, extensions) {
3078
- for (const [ name, fn ] of extensions) {
3079
- if ((typeof fn) !== 'function') {
3080
- // Nested structure is allowed
3081
- context[name] = context[name] || {};
3082
- return wrap(context[name], fn);
3083
- }
3084
- const superMethod = context[name];
3085
- context[name] = function(...args) {
3086
- return fn(superMethod, ...args);
3087
- };
3088
- }
3089
- }
@@ -0,0 +1,21 @@
1
+ module.exports = extendQueries;
2
+
3
+ function extendQueries(queries, extensions) {
4
+ for (const [ name, fn ] of Object.entries(extensions)) {
5
+ if (typeof fn === 'object' && !Array.isArray(fn) && fn !== null) {
6
+ // Nested structure is allowed
7
+ queries[name] = queries[name] || {};
8
+ return extendQueries(queries[name], fn);
9
+ }
10
+
11
+ if (typeof fn !== 'function' || typeof queries[name] !== 'function') {
12
+ queries[name] = fn;
13
+ continue;
14
+ }
15
+
16
+ const superMethod = queries[name];
17
+ queries[name] = function(...args) {
18
+ return fn(superMethod, ...args);
19
+ };
20
+ }
21
+ };
@@ -775,7 +775,7 @@ export default {
775
775
  window.localStorage.setItem(this.savePreferenceName, pref);
776
776
  },
777
777
  onContentChanged(e) {
778
- if (this.original?._id !== e.doc._id) {
778
+ if (!e.doc || this.original?._id !== e.doc._id) {
779
779
  return;
780
780
  }
781
781
  if (e.doc.type !== this.docType) {
@@ -1,5 +1,5 @@
1
1
  const nodemailer = require('nodemailer');
2
- const htmlToText = require('html-to-text');
2
+ const { htmlToText } = require('html-to-text');
3
3
  const _ = require('lodash');
4
4
 
5
5
  // ## Options
@@ -26,18 +26,61 @@ module.exports = {
26
26
  async emailForModule(req, templateName, data, options, module) {
27
27
  const transport = self.getTransport();
28
28
  const html = await module.render(req, templateName, data);
29
- const text = htmlToText.fromString(html, {
30
- format: {
31
- heading: function (elem, fn, options) {
32
- const h = fn(elem.children, options);
33
- const split = h.split(/(\[.*?\])/);
34
- return split.map(function (s) {
35
- if (s.match(/^\[.*\]$/)) {
36
- return s;
37
- } else {
38
- return s.toUpperCase();
39
- }
40
- }).join('') + '\n';
29
+ const text = htmlToText(html, {
30
+ selectors: [
31
+ {
32
+ selector: 'a',
33
+ options: { hideLinkHrefIfSameAsText: true }
34
+ },
35
+ {
36
+ selector: 'h1',
37
+ format: 'customHeading'
38
+ },
39
+ {
40
+ selector: 'h2',
41
+ format: 'customHeading'
42
+ },
43
+ {
44
+ selector: 'h3',
45
+ format: 'customHeading'
46
+ },
47
+ {
48
+ selector: 'h4',
49
+ format: 'customHeading'
50
+ },
51
+ {
52
+ selector: 'h5',
53
+ format: 'customHeading'
54
+ },
55
+ {
56
+ selector: 'h6',
57
+ format: 'customHeading'
58
+ }
59
+ ],
60
+ formatters: {
61
+ // Custom heading formatter, based on the core heading but
62
+ // no uppercase inside "[ ]" blocks.
63
+ customHeading: function (elem, walk, builder, formatOptions) {
64
+ builder.openBlock({ leadingLineBreaks: formatOptions.leadingLineBreaks || 2 });
65
+ if (formatOptions.uppercase !== false) {
66
+ // Keep track of [ and ] (no nested support)
67
+ let ignoreUpper = false;
68
+ builder.pushWordTransform(str => {
69
+ if (str.trim().startsWith('[')) {
70
+ ignoreUpper = true;
71
+ }
72
+ const word = ignoreUpper ? str : str.toUpperCase();
73
+ if (str.trim().endsWith(']')) {
74
+ ignoreUpper = false;
75
+ }
76
+ return word;
77
+ });
78
+ walk(elem.children, builder);
79
+ builder.popWordTransform();
80
+ } else {
81
+ walk(elem.children, builder);
82
+ }
83
+ builder.closeBlock({ trailingLineBreaks: formatOptions.trailingLineBreaks || 2 });
41
84
  }
42
85
  }
43
86
  });
@@ -140,13 +140,21 @@ export default {
140
140
  const fields = this.getFieldsByCategory(followedByCategory);
141
141
 
142
142
  const followingValues = {};
143
+ const parentFollowing = {};
144
+ for (const [ key, val ] of Object.entries(this.parentFollowingValues || {})) {
145
+ parentFollowing[`<${key}`] = val;
146
+ }
143
147
 
144
148
  for (const field of fields) {
145
149
  if (field.following) {
146
150
  const following = Array.isArray(field.following) ? field.following : [ field.following ];
147
151
  followingValues[field.name] = {};
148
152
  for (const name of following) {
149
- followingValues[field.name][name] = this.getFieldValue(name);
153
+ if (name.startsWith('<')) {
154
+ followingValues[field.name][name] = parentFollowing[name];
155
+ } else {
156
+ followingValues[field.name][name] = this.getFieldValue(name);
157
+ }
150
158
  }
151
159
  }
152
160
  }
@@ -8,10 +8,14 @@
8
8
  <div class="apos-input-wrapper">
9
9
  <input
10
10
  :class="classes"
11
- v-model="next.url" type="url"
11
+ v-model="next.url"
12
+ type="url"
12
13
  :placeholder="$t(field.placeholder)"
13
- :disabled="field.readOnly" :required="field.required"
14
- :id="uid" :tabindex="tabindex"
14
+ :disabled="field.readOnly"
15
+ :readonly="tempReadOnly"
16
+ :required="field.required"
17
+ :id="uid"
18
+ :tabindex="tabindex"
15
19
  >
16
20
  <component
17
21
  v-if="icon"
@@ -43,10 +47,14 @@ export default {
43
47
  data () {
44
48
  return {
45
49
  next: (this.value && this.value.data)
46
- ? this.value.data : {},
50
+ ? { ...this.value.data } : {},
47
51
  oembedResult: {},
48
52
  dynamicRatio: '',
49
- oembedError: null
53
+ oembedError: null,
54
+
55
+ // This variable will set the input as readonly,
56
+ // not disabled, in order to avoid losing focus.
57
+ tempReadOnly: false
50
58
  };
51
59
  },
52
60
  computed: {
@@ -104,7 +112,7 @@ export default {
104
112
  this.validateAndEmit();
105
113
  },
106
114
  async loadOembed () {
107
- this.field.readOnly = true;
115
+ this.tempReadOnly = true;
108
116
  this.oembedResult = {};
109
117
  this.oembedError = null;
110
118
  this.dynamicRatio = '';
@@ -132,7 +140,7 @@ export default {
132
140
  this.next.title = '';
133
141
  this.next.thumbnail = '';
134
142
  } finally {
135
- this.field.readOnly = false;
143
+ this.tempReadOnly = false;
136
144
  }
137
145
  }
138
146
  }
@@ -73,7 +73,7 @@
73
73
  :checked-count="checked.length"
74
74
  :batch-operations="moduleOptions.batchOperations"
75
75
  @select-click="selectAll"
76
- @search="search"
76
+ @search="onSearch"
77
77
  @page-change="updatePage"
78
78
  @filter="filter"
79
79
  @batch="handleBatchAction"
@@ -119,6 +119,7 @@
119
119
  import AposDocsManagerMixin from 'Modules/@apostrophecms/modal/mixins/AposDocsManagerMixin';
120
120
  import AposModifiedMixin from 'Modules/@apostrophecms/ui/mixins/AposModifiedMixin';
121
121
  import AposPublishMixin from 'Modules/@apostrophecms/ui/mixins/AposPublishMixin';
122
+ import { debounce } from 'Modules/@apostrophecms/ui/utils';
122
123
 
123
124
  export default {
124
125
  name: 'AposDocsManager',
@@ -145,7 +146,9 @@ export default {
145
146
  totalPages: 1,
146
147
  currentPage: 1,
147
148
  filterValues: {},
148
- queryExtras: {},
149
+ queryExtras: {
150
+ viewContext: this.relationshipField ? 'relationship' : 'manage'
151
+ },
149
152
  holdQueries: false,
150
153
  filterChoices: {},
151
154
  allPiecesSelection: {
@@ -209,6 +212,9 @@ export default {
209
212
  }
210
213
  },
211
214
  created() {
215
+ const DEBOUNCE_TIMEOUT = 500;
216
+ this.onSearch = debounce(this.search, DEBOUNCE_TIMEOUT);
217
+
212
218
  this.moduleOptions.filters.forEach(filter => {
213
219
  this.filterValues[filter.name] = filter.def;
214
220
  if (!filter.choices) {
@@ -1241,6 +1241,9 @@ module.exports = {
1241
1241
  if (fieldType.validate) {
1242
1242
  fieldType.validate(field, options, warn, fail);
1243
1243
  }
1244
+ // Ancestors hoisting should happen AFTER the validation recursion,
1245
+ // so that ancestors are processed as well.
1246
+ self.hoistFollowingFieldsToParent(field, parent);
1244
1247
  function fail(s) {
1245
1248
  throw new Error(format(s));
1246
1249
  }
@@ -1261,6 +1264,28 @@ module.exports = {
1261
1264
  }
1262
1265
  },
1263
1266
 
1267
+ // If a field has a following property and a parent,
1268
+ // hoist that property values to the parent,
1269
+ // if they start with `<`.
1270
+ hoistFollowingFieldsToParent(field, parent) {
1271
+ if (!parent || !field.following) {
1272
+ return;
1273
+ }
1274
+ const following = typeof field.following === 'string'
1275
+ ? [ field.following ]
1276
+ : field.following;
1277
+ const parentFollowing = typeof parent.following === 'string'
1278
+ ? [ parent.following ]
1279
+ : parent.following;
1280
+ const hoistFollowing = following
1281
+ .filter(f => f.startsWith('<'))
1282
+ .map(f => f.slice(1));
1283
+ parent.following = _.uniq([
1284
+ ...(parentFollowing || []),
1285
+ ...hoistFollowing
1286
+ ]);
1287
+ },
1288
+
1264
1289
  // Recursively register the given schema, giving each field an _id and making provision to be able to
1265
1290
  // fetch its definition via apos.schema.getFieldById().
1266
1291
  //
@@ -114,6 +114,10 @@ export default {
114
114
  docId: {
115
115
  type: String,
116
116
  default: null
117
+ },
118
+ parentFollowingValues: {
119
+ type: Object,
120
+ default: null
117
121
  }
118
122
  },
119
123
  emits: [ 'modal-result', 'safe-close' ],
@@ -141,6 +141,7 @@
141
141
 
142
142
  <script>
143
143
  import AposInputMixin from 'Modules/@apostrophecms/schema/mixins/AposInputMixin.js';
144
+ import AposInputFollowingMixin from 'Modules/@apostrophecms/schema/mixins/AposInputFollowingMixin.js';
144
145
  import cuid from 'cuid';
145
146
  import { klona } from 'klona';
146
147
  import { get } from 'lodash';
@@ -149,7 +150,7 @@ import draggable from 'vuedraggable';
149
150
  export default {
150
151
  name: 'AposInputArray',
151
152
  components: { draggable },
152
- mixins: [ AposInputMixin ],
153
+ mixins: [ AposInputMixin, AposInputFollowingMixin ],
153
154
  props: {
154
155
  generation: {
155
156
  type: Number,
@@ -288,7 +289,8 @@ export default {
288
289
  field: this.field,
289
290
  items: this.next,
290
291
  serverError: this.serverError,
291
- docId: this.docId
292
+ docId: this.docId,
293
+ parentFollowingValues: this.followingValues
292
294
  });
293
295
  if (result) {
294
296
  this.next = result;
@@ -341,18 +343,7 @@ export default {
341
343
  });
342
344
  },
343
345
  getFollowingValues(item) {
344
- const followingValues = {};
345
- for (const field of this.field.schema) {
346
- if (field.following) {
347
- const following = Array.isArray(field.following) ? field.following : [ field.following ];
348
- followingValues[field.name] = {};
349
- for (const name of following) {
350
- followingValues[field.name][name] = item.schemaInput.data[name];
351
- }
352
- }
353
- }
354
-
355
- return followingValues;
346
+ return this.computeFollowingValues(item.schemaInput.data);
356
347
  }
357
348
  }
358
349
  };
@@ -17,6 +17,7 @@
17
17
  :generation="generation"
18
18
  :doc-id="docId"
19
19
  v-model="schemaInput"
20
+ :following-values="followingValuesWithParent"
20
21
  ref="schema"
21
22
  />
22
23
  </div>
@@ -27,10 +28,11 @@
27
28
 
28
29
  <script>
29
30
  import AposInputMixin from 'Modules/@apostrophecms/schema/mixins/AposInputMixin.js';
31
+ import AposInputFollowingMixin from 'Modules/@apostrophecms/schema/mixins/AposInputFollowingMixin.js';
30
32
 
31
33
  export default {
32
34
  name: 'AposInputObject',
33
- mixins: [ AposInputMixin ],
35
+ mixins: [ AposInputMixin, AposInputFollowingMixin ],
34
36
  props: {
35
37
  generation: {
36
38
  type: Number,
@@ -56,6 +58,11 @@ export default {
56
58
  next
57
59
  };
58
60
  },
61
+ computed: {
62
+ followingValuesWithParent() {
63
+ return this.computeFollowingValues(this.schemaInput.data);
64
+ }
65
+ },
59
66
  watch: {
60
67
  schemaInput: {
61
68
  deep: true,
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Provides followingValues computation for fields having
3
+ * sub-schema (array, object).
4
+ */
5
+
6
+ export default {
7
+ methods: {
8
+ // Accept a `data` object with field values and return an object
9
+ // of the following values for each field of the underlying schema.
10
+ computeFollowingValues(data) {
11
+ const followingValues = {};
12
+ const parentFollowing = {};
13
+ for (const [ key, val ] of Object.entries(this.followingValues || {})) {
14
+ parentFollowing[`<${key}`] = val;
15
+ }
16
+
17
+ for (const field of this.field.schema) {
18
+ if (field.following) {
19
+ const following = Array.isArray(field.following) ? field.following : [ field.following ];
20
+ followingValues[field.name] = {};
21
+ for (const name of following) {
22
+ if (name.startsWith('<')) {
23
+ followingValues[field.name][name] = parentFollowing[name];
24
+ } else {
25
+ followingValues[field.name][name] = data[name];
26
+ }
27
+ }
28
+ }
29
+ }
30
+
31
+ return followingValues;
32
+ }
33
+ }
34
+ };
@@ -64,6 +64,7 @@
64
64
  // Type Sizes
65
65
  --a-type-tiny: 9px;
66
66
  --a-type-small: 10px;
67
+ --a-type-smaller: 11px;
67
68
  --a-type-base: 12px;
68
69
  --a-type-label: 13px;
69
70
  --a-type-large: 14px;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apostrophe",
3
- "version": "3.45.1",
3
+ "version": "3.47.0",
4
4
  "description": "The Apostrophe Content Management System.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -17,7 +17,7 @@
17
17
  "url": "git@github.com:apostrophecms/apostrophe.git"
18
18
  },
19
19
  "engines": {
20
- "node": ">=12.0.0"
20
+ "node": ">=16.0.0"
21
21
  },
22
22
  "keywords": [
23
23
  "apostrophe",
@@ -75,12 +75,12 @@
75
75
  "fs-extra": "^7.0.1",
76
76
  "glob": "^5.0.15",
77
77
  "he": "^0.5.0",
78
- "html-to-text": "^5.1.1",
78
+ "html-to-text": "^9.0.5",
79
79
  "i18next": "^20.3.2",
80
80
  "i18next-http-middleware": "^3.1.5",
81
81
  "import-fresh": "^3.3.0",
82
82
  "is-wsl": "^2.2.0",
83
- "jsdom": "^17.0.0",
83
+ "jsdom": "^22.0.0",
84
84
  "klona": "^2.0.4",
85
85
  "launder": "^1.4.0",
86
86
  "lodash": "^4.17.20",
@@ -109,11 +109,11 @@
109
109
  "sass": "^1.52.3",
110
110
  "sass-loader": "^10.1.1",
111
111
  "server-destroy": "^1.0.1",
112
- "sluggo": "^0.3.0",
112
+ "sluggo": "^1.0.0",
113
113
  "tinycolor2": "^1.4.2",
114
114
  "tough-cookie": "^4.0.0",
115
115
  "underscore.string": "^3.3.4",
116
- "uploadfs": "^1.17.1",
116
+ "uploadfs": "^1.22.1",
117
117
  "v-tooltip": "^2.0.3",
118
118
  "vue": "^2.6.14",
119
119
  "vue-advanced-cropper": "^1.10.1",
@@ -0,0 +1,30 @@
1
+ module.exports = {
2
+ html: '<h4>[test string] Resetting your [ another test string ] password on localhost</h4>\n' +
3
+ '<p>Hello cy_admin,</p>\n' +
4
+ '<p>You are receiving this email because a request to reset your password was made on localhost.</p>\n' +
5
+ '<p>If that is your wish, please follow this link to complete the password reset process:</p>\n' +
6
+ '<p><a href="http://localhost:3000/login?reset=clh0f780t0009ezh97wtu03d0&amp;email=admin%40example.com">http://localhost:3000/login?reset=clh0f780t0009ezh97wtu03d0&amp;email=admin%40example.com</a></p>\n' +
7
+ '<p>\n' +
8
+ ' If you did not request to reset your password, please delete and ignore this email. Someone else may have entered\n' +
9
+ ' your email address in error.\n' +
10
+ '</p>\n',
11
+ text: '[test string] RESETTING YOUR [ another test string ] PASSWORD ON LOCALHOST\n' +
12
+ '\n' +
13
+ 'Hello cy_admin,\n' +
14
+ '\n' +
15
+ 'You are receiving this email because a request to reset your password was made\n' +
16
+ 'on localhost.\n' +
17
+ '\n' +
18
+ 'If that is your wish, please follow this link to complete the password reset\n' +
19
+ 'process:\n' +
20
+ '\n' +
21
+ 'http://localhost:3000/login?reset=clh0f780t0009ezh97wtu03d0&email=admin%40example.com\n' +
22
+ // Improved link formatting via hideLinkHrefIfSameAsText option
23
+ // '[http://localhost:3000/login?reset=clh0f780t0009ezh97wtu03d0&email=admin%40example.com]\n' +
24
+ '\n' +
25
+ 'If you did not request to reset your password, please delete and ignore this\n' +
26
+ 'email. Someone else may have entered your email address in error.',
27
+ from: 'noreply@example.com',
28
+ to: 'admin@example.com',
29
+ subject: 'Your request to reset your password on localhost'
30
+ };
package/test/email.js CHANGED
@@ -1,5 +1,5 @@
1
1
  const t = require('../test-lib/test.js');
2
- const assert = require('assert');
2
+ const assert = require('assert').strict;
3
3
  let apos;
4
4
 
5
5
  describe('Email', function() {
@@ -52,4 +52,35 @@ describe('Email', function() {
52
52
  assert(message.match(/To: recipient@example\.com/));
53
53
  assert(message.match(/\[http:\/\/example\.com\/\]/));
54
54
  });
55
+
56
+ it('should convert html to text', async function () {
57
+ await t.destroy(apos);
58
+ apos = await t.create({
59
+ root: module
60
+ });
61
+
62
+ const mockEmail = require('./data/fpw_email_mock.js');
63
+ const mockTransport = () => ({
64
+ sendMail: function (args) {
65
+ return Promise.resolve(args);
66
+ }
67
+ });
68
+ const mockModule = {
69
+ options: { email: { from: mockEmail.from } },
70
+ render: async () => mockEmail.html
71
+ };
72
+ apos.modules['@apostrophecms/email'].getTransport = mockTransport;
73
+
74
+ const result = await apos.modules['@apostrophecms/email'].emailForModule(
75
+ apos.task.getReq(), // req
76
+ 'anyTemplate', // templateName
77
+ {}, // data
78
+ {
79
+ to: mockEmail.to,
80
+ subject: mockEmail.subject
81
+ }, // options
82
+ mockModule // module
83
+ );
84
+ assert.equal(result.text, mockEmail.text);
85
+ });
55
86
  });
@@ -0,0 +1,165 @@
1
+ const t = require('../test-lib/test.js');
2
+ const assert = require('assert');
3
+
4
+ describe('Schema - follow ancestor fields', function() {
5
+
6
+ let apos;
7
+ this.timeout(t.timeout);
8
+ this.slow(2000);
9
+
10
+ beforeEach(async function() {
11
+ return t.destroy(apos);
12
+ });
13
+
14
+ after(async function() {
15
+ return t.destroy(apos);
16
+ });
17
+
18
+ it('should follow a parent field in array and object', async function() {
19
+ apos = await t.create({
20
+ root: module,
21
+ modules: {
22
+ household: {
23
+ extend: '@apostrophecms/piece-type',
24
+ options: {
25
+ alias: 'household'
26
+ },
27
+ fields: {
28
+ add: {
29
+ petPreference: {
30
+ type: 'select',
31
+ label: 'Pet Preference',
32
+ choices: [
33
+ {
34
+ value: 'cats',
35
+ label: 'Cats'
36
+ },
37
+ {
38
+ value: 'dogs',
39
+ label: 'Dogs'
40
+ }
41
+ ],
42
+ required: true
43
+ },
44
+ pets: {
45
+ type: 'array',
46
+ label: 'Pets',
47
+ fields: {
48
+ add: {
49
+ name: {
50
+ type: 'string',
51
+ label: 'Name',
52
+ // Follow one level up
53
+ following: [ '<petPreference' ]
54
+ }
55
+ }
56
+ }
57
+ },
58
+ favoritePet: {
59
+ type: 'object',
60
+ label: 'Favorite Pet',
61
+ fields: {
62
+ add: {
63
+ name: {
64
+ type: 'string',
65
+ label: 'Name',
66
+ // Follow one level up
67
+ following: [ '<petPreference' ]
68
+ }
69
+ }
70
+ }
71
+ }
72
+ }
73
+ }
74
+ }
75
+ }
76
+ });
77
+ const schema = apos.modules.household.schema;
78
+ const petField = schema.find(field => field.name === 'pets');
79
+ const favPetField = schema.find(field => field.name === 'favoritePet');
80
+ assert(petField);
81
+ assert.deepEqual(petField.following, [ 'petPreference' ]);
82
+ assert(favPetField);
83
+ assert.deepEqual(favPetField.following, [ 'petPreference' ]);
84
+ });
85
+
86
+ it('should follow a grand parent field in array and object', async function() {
87
+ apos = await t.create({
88
+ root: module,
89
+ modules: {
90
+ household: {
91
+ extend: '@apostrophecms/piece-type',
92
+ options: {
93
+ alias: 'household'
94
+ },
95
+ fields: {
96
+ add: {
97
+ petPreference: {
98
+ type: 'select',
99
+ label: 'Pet Preference',
100
+ choices: [
101
+ {
102
+ value: 'cats',
103
+ label: 'Cats'
104
+ },
105
+ {
106
+ value: 'dogs',
107
+ label: 'Dogs'
108
+ }
109
+ ],
110
+ required: true
111
+ },
112
+ rooms: {
113
+ type: 'array',
114
+ label: 'Rooms',
115
+ fields: {
116
+ add: {
117
+ pets: {
118
+ type: 'array',
119
+ label: 'Pets',
120
+ fields: {
121
+ add: {
122
+ name: {
123
+ type: 'string',
124
+ label: 'Name',
125
+ // Follow two levels up
126
+ following: [ '<<petPreference' ]
127
+ }
128
+ }
129
+ }
130
+ },
131
+ favoritePet: {
132
+ type: 'object',
133
+ label: 'Favorite Pet',
134
+ fields: {
135
+ add: {
136
+ name: {
137
+ type: 'string',
138
+ label: 'Name',
139
+ // Follow two levels up
140
+ following: [ '<<petPreference' ]
141
+ }
142
+ }
143
+ }
144
+ }
145
+ }
146
+ }
147
+ }
148
+ }
149
+ }
150
+ }
151
+ }
152
+ });
153
+ const schema = apos.modules.household.schema;
154
+ const rooms = schema.find(field => field.name === 'rooms');
155
+ assert(rooms);
156
+ assert.deepEqual(rooms.following, [ 'petPreference' ]);
157
+
158
+ const petField = rooms.schema.find(field => field.name === 'pets');
159
+ const favPetField = rooms.schema.find(field => field.name === 'favoritePet');
160
+ assert(petField);
161
+ assert.deepEqual(petField.following, [ '<petPreference' ]);
162
+ assert(favPetField);
163
+ assert.deepEqual(favPetField.following, [ '<petPreference' ]);
164
+ });
165
+ });
@@ -0,0 +1,225 @@
1
+ const t = require('../test-lib/test.js');
2
+ const assert = require('assert');
3
+
4
+ describe('Query Builders', function() {
5
+ this.timeout(t.timeout);
6
+
7
+ let apos;
8
+ after(function() {
9
+ return t.destroy(apos);
10
+ });
11
+
12
+ before(async function () {
13
+ apos = await t.create({
14
+ root: module,
15
+ modules: {
16
+ young: {
17
+ options: {
18
+ alias: 'young'
19
+ },
20
+ extend: '@apostrophecms/piece-type',
21
+ fields: {
22
+ add: {
23
+ age: {
24
+ label: 'Age',
25
+ type: 'integer',
26
+ required: true
27
+ }
28
+ }
29
+ },
30
+ queries(self, query) {
31
+ return {
32
+ builders: {
33
+ age: {
34
+ launder(str) {
35
+ return [ 'children', 'adult' ].includes(str) ? str : null;
36
+ },
37
+ finalize() {
38
+ const age = query.get('age');
39
+
40
+ if ([ 'children', 'adults' ].includes(age)) {
41
+ const ageCriteria = age === 'children' ? { $lte: 18 } : { $gt: 18 };
42
+ query.and({ age: ageCriteria });
43
+ }
44
+ }
45
+ }
46
+ },
47
+ methods: {
48
+ async sortByAge() {
49
+ await query.finalize();
50
+
51
+ const pipeline = [
52
+ { $match: query.get('criteria') },
53
+ { $sort: { age: 1 } }
54
+ ];
55
+
56
+ const results = await self.apos.doc.db.aggregate(pipeline).toArray();
57
+
58
+ return results;
59
+ }
60
+ }
61
+ };
62
+ }
63
+ },
64
+ person: {
65
+ extend: 'young',
66
+ options: {
67
+ alias: 'person'
68
+ },
69
+ extendQueries(self, query) {
70
+ return {
71
+ builders: {
72
+ age: {
73
+ def: 'adult',
74
+ launder(_super, val) {
75
+ const laundered = _super();
76
+
77
+ if (laundered !== null) {
78
+ return laundered;
79
+ }
80
+
81
+ return val === 'senior' ? val : null;
82
+ },
83
+ async finalize(_super) {
84
+ await _super();
85
+
86
+ const age = query.get('age');
87
+
88
+ if (age === 'seniors') {
89
+ query.and({ age: { $gt: 60 } });
90
+ }
91
+ }
92
+ }
93
+ },
94
+ methods: {
95
+ async sortByAge(_super) {
96
+ assert(typeof _super === 'function');
97
+
98
+ await query.finalize();
99
+
100
+ const pipeline = [
101
+ { $match: query.get('criteria') },
102
+ { $sort: { age: -1 } }
103
+ ];
104
+
105
+ const results = await self.apos.doc.db.aggregate(pipeline).toArray();
106
+
107
+ return results;
108
+ }
109
+ }
110
+ };
111
+ }
112
+ }
113
+ }
114
+ });
115
+ });
116
+
117
+ it('should insert person pieces and verify age query builder is working', async function() {
118
+ const req = apos.task.getReq();
119
+ const persons = getPersons(apos.young);
120
+ const { insertedCount } = await apos.doc.db.insertMany(persons);
121
+
122
+ assert(insertedCount === 6);
123
+
124
+ const children = await apos.young.find(req).age('children').toArray();
125
+ const adults = await apos.young.find(req).age('adults').toArray();
126
+
127
+ assert(children.length === 2);
128
+ children.forEach((child) => {
129
+ assert(child.age <= 18);
130
+ });
131
+
132
+ assert(adults.length === 4);
133
+ adults.forEach((adult) => {
134
+ assert(adult.age > 18);
135
+ });
136
+ });
137
+
138
+ it('should insert seniors and verify the query builders have been properly extended', async function() {
139
+ const req = apos.task.getReq();
140
+ const persons = getPersons(apos.person, true);
141
+ const { insertedCount } = await apos.doc.db.insertMany(persons);
142
+
143
+ assert(insertedCount === 8);
144
+
145
+ const children = await apos.person.find(req).age('children').toArray();
146
+ const adults = await apos.person.find(req).age('adults').toArray();
147
+ const seniors = await apos.person.find(req).age('seniors').toArray();
148
+
149
+ assert(children.length === 2);
150
+ children.forEach((child) => {
151
+ assert(child.age <= 18);
152
+ });
153
+
154
+ assert(adults.length === 6);
155
+ adults.forEach((adult) => {
156
+ assert(adult.age > 18);
157
+ });
158
+
159
+ assert(seniors.length === 2);
160
+ seniors.forEach((senior) => {
161
+ assert(senior.age > 60);
162
+ });
163
+ });
164
+
165
+ it('should verify that query methods work and can be extende', async function() {
166
+ const req = apos.task.getReq();
167
+ const youngSorted = await apos.young.find(req).age('adults').sortByAge();
168
+ assert(youngSorted[0].age === 25);
169
+ assert(youngSorted[1].age === 32);
170
+ assert(youngSorted[2].age === 50);
171
+ assert(youngSorted[3].age === 58);
172
+
173
+ const personsSorted = await apos.person.find(req).age('adults').sortByAge();
174
+ assert(personsSorted[0].age === 80);
175
+ assert(personsSorted[1].age === 72);
176
+ assert(personsSorted[2].age === 58);
177
+ assert(personsSorted[3].age === 50);
178
+ assert(personsSorted[4].age === 32);
179
+ });
180
+ });
181
+
182
+ function getPersons(instance, withSeniors = false) {
183
+ const moduleName = instance.__meta.name;
184
+ return [
185
+ {
186
+ title: 'Jean',
187
+ age: 32
188
+ },
189
+ {
190
+ title: 'Julie',
191
+ age: 25
192
+ },
193
+ {
194
+ title: 'Victor',
195
+ age: 14
196
+ },
197
+ {
198
+ title: 'Marc',
199
+ age: 58
200
+ },
201
+ {
202
+ title: 'Hector',
203
+ age: 7
204
+ },
205
+ {
206
+ title: 'Marie',
207
+ age: 50
208
+ },
209
+ ...withSeniors ? [
210
+ {
211
+ title: 'Jules',
212
+ age: 72
213
+ },
214
+ {
215
+ title: 'Renée',
216
+ age: 80
217
+ }
218
+ ] : []
219
+ ].map((p, i) => ({
220
+ _id: `${moduleName}${i}`,
221
+ ...instance.newInstance(),
222
+ slug: `${moduleName}-${p.title.toLowerCase()}`,
223
+ ...p
224
+ }));
225
+ }