apostrophe 3.46.0 → 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,22 @@
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
+
3
20
  ## 3.46.0 (2023-05-03)
4
21
 
5
22
  ### Fixes
@@ -58,7 +75,11 @@ shouldn't close the link dialog etc.
58
75
 
59
76
  ### Fixes
60
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.
61
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.
62
83
 
63
84
  ## 3.44.0 (2023-04-13)
64
85
 
@@ -78,11 +99,6 @@ those writing mocha tests of Apostrophe modules.
78
99
  ### Fixes
79
100
  * Fix child page slug when title is deleted
80
101
 
81
- ### Fixes
82
-
83
- * 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.
84
- 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.
85
-
86
102
  ## 3.43.0 (2023-03-29)
87
103
 
88
104
  ### Adds
@@ -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
+ };
@@ -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
  }
@@ -146,7 +146,9 @@ export default {
146
146
  totalPages: 1,
147
147
  currentPage: 1,
148
148
  filterValues: {},
149
- queryExtras: {},
149
+ queryExtras: {
150
+ viewContext: this.relationshipField ? 'relationship' : 'manage'
151
+ },
150
152
  holdQueries: false,
151
153
  filterChoices: {},
152
154
  allPiecesSelection: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apostrophe",
3
- "version": "3.46.0",
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",
@@ -80,7 +80,7 @@
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,7 +109,7 @@
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",
@@ -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
+ }