apostrophe 3.45.0 → 3.46.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 +27 -0
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaEditor.vue +5 -0
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaExpandedMenu.vue +16 -5
- package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +1 -1
- package/modules/@apostrophecms/email/index.js +56 -13
- package/modules/@apostrophecms/modal/ui/apos/apps/AposModals.js +13 -0
- package/modules/@apostrophecms/modal/ui/apos/mixins/AposEditorMixin.js +9 -1
- package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +5 -1
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +6 -0
- package/modules/@apostrophecms/schema/index.js +25 -0
- package/modules/@apostrophecms/schema/ui/apos/components/AposArrayEditor.vue +4 -0
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputArray.vue +5 -14
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputObject.vue +8 -1
- package/modules/@apostrophecms/schema/ui/apos/mixins/AposInputFollowingMixin.js +34 -0
- package/modules/@apostrophecms/ui/ui/apos/scss/global/_theme.scss +1 -0
- package/package.json +3 -3
- package/test/data/fpw_email_mock.js +30 -0
- package/test/email.js +32 -1
- package/test/follow-ancestors.js +165 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.46.0 (2023-05-03)
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
|
|
7
|
+
* 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
|
|
8
|
+
when working with the `@apostrophecms/ai-helper` module, and also helps in other situations.
|
|
9
|
+
* Check that `e.doc` exists when handling `content-changed` event.
|
|
10
|
+
* Require updated `uploadfs` version with no dependency warnings.
|
|
11
|
+
|
|
12
|
+
### Adds
|
|
13
|
+
|
|
14
|
+
* 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.
|
|
15
|
+
|
|
16
|
+
### Changes
|
|
17
|
+
|
|
18
|
+
* Debounce search to prevent calling search on every key stroke in the manager modal.
|
|
19
|
+
* Various size and spacing adjustments in the expanded Add Content modal UI
|
|
20
|
+
|
|
21
|
+
## 3.45.1 (2023-04-28)
|
|
22
|
+
|
|
23
|
+
### Fixes
|
|
24
|
+
|
|
25
|
+
* Added missing styles to ensure consistent presentation of the rich text insert menu.
|
|
26
|
+
* Fixed a bug in which clicking on an image in the media manager would close the "insert
|
|
27
|
+
image" dialog box.
|
|
28
|
+
* Update `html-to-text` package to the latest major version.
|
|
29
|
+
|
|
3
30
|
## 3.45.0 (2023-04-27)
|
|
4
31
|
|
|
5
32
|
### 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:
|
|
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
|
-
|
|
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>
|
|
@@ -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
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
});
|
|
@@ -41,7 +41,20 @@ export default function() {
|
|
|
41
41
|
// or el2 is not a modal, it is treated as its DOM
|
|
42
42
|
// parent modal, or as `document`. If el1 has no
|
|
43
43
|
// parent modal this method always returns false.
|
|
44
|
+
//
|
|
45
|
+
// If el1 is no longer connected to the DOM then it
|
|
46
|
+
// is also considered to be "on top" e.g. not something
|
|
47
|
+
// that should concern `v-click-outside-element` and
|
|
48
|
+
// similar functionality. This is necessary because
|
|
49
|
+
// sometimes Vue removes elements from the DOM before
|
|
50
|
+
// we can examine their relationships.
|
|
44
51
|
onTopOf(el1, el2) {
|
|
52
|
+
if (!el1.isConnected) {
|
|
53
|
+
// If el1 is no longer in the DOM we can't make a proper determination,
|
|
54
|
+
// returning true prevents unwanted things like click-outside-element
|
|
55
|
+
// events from firing
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
45
58
|
if (!el1.matches('[data-apos-modal]')) {
|
|
46
59
|
el1 = el1.closest('[data-apos-modal]') || document;
|
|
47
60
|
}
|
|
@@ -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
|
-
|
|
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
|
}
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
:checked-count="checked.length"
|
|
74
74
|
:batch-operations="moduleOptions.batchOperations"
|
|
75
75
|
@select-click="selectAll"
|
|
76
|
-
@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',
|
|
@@ -209,6 +210,9 @@ export default {
|
|
|
209
210
|
}
|
|
210
211
|
},
|
|
211
212
|
created() {
|
|
213
|
+
const DEBOUNCE_TIMEOUT = 500;
|
|
214
|
+
this.onSearch = debounce(this.search, DEBOUNCE_TIMEOUT);
|
|
215
|
+
|
|
212
216
|
this.moduleOptions.filters.forEach(filter => {
|
|
213
217
|
this.filterValues[filter.name] = filter.def;
|
|
214
218
|
if (!filter.choices) {
|
package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue
CHANGED
|
@@ -678,6 +678,7 @@ function traverseNextNode(node) {
|
|
|
678
678
|
border: 1px solid var(--a-base-8);
|
|
679
679
|
color: var(--a-base-1);
|
|
680
680
|
font-family: var(--a-family-default);
|
|
681
|
+
font-size: var(--a-type-base);
|
|
681
682
|
}
|
|
682
683
|
|
|
683
684
|
.apos-rich-text-insert-menu-item {
|
|
@@ -694,6 +695,11 @@ function traverseNextNode(node) {
|
|
|
694
695
|
flex-direction: column;
|
|
695
696
|
h4, p {
|
|
696
697
|
margin: 4px;
|
|
698
|
+
font-family: var(--a-family-default);
|
|
699
|
+
font-size: var(--a-type-base);
|
|
700
|
+
}
|
|
701
|
+
h4 {
|
|
702
|
+
font-weight: bold;
|
|
697
703
|
}
|
|
698
704
|
}
|
|
699
705
|
.apos-rich-text-insert-menu-icon {
|
|
@@ -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
|
//
|
|
@@ -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
|
-
|
|
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
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apostrophe",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.46.0",
|
|
4
4
|
"description": "The Apostrophe Content Management System.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"fs-extra": "^7.0.1",
|
|
76
76
|
"glob": "^5.0.15",
|
|
77
77
|
"he": "^0.5.0",
|
|
78
|
-
"html-to-text": "^
|
|
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",
|
|
@@ -113,7 +113,7 @@
|
|
|
113
113
|
"tinycolor2": "^1.4.2",
|
|
114
114
|
"tough-cookie": "^4.0.0",
|
|
115
115
|
"underscore.string": "^3.3.4",
|
|
116
|
-
"uploadfs": "^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&email=admin%40example.com">http://localhost:3000/login?reset=clh0f780t0009ezh97wtu03d0&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
|
+
});
|