apostrophe 3.46.0 → 3.48.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/.github/workflows/main.yml +2 -2
- package/CHANGELOG.md +51 -5
- package/lib/moog-require.js +4 -0
- package/modules/@apostrophecms/any-page-type/index.js +5 -0
- package/modules/@apostrophecms/doc-type/index.js +42 -17
- package/modules/@apostrophecms/doc-type/lib/extendQueries.js +21 -0
- package/modules/@apostrophecms/image/index.js +8 -0
- package/modules/@apostrophecms/oembed-field/ui/apos/components/AposInputOembed.vue +15 -7
- package/modules/@apostrophecms/page-type/index.js +6 -0
- package/modules/@apostrophecms/piece-type/index.js +36 -0
- package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +7 -1
- package/modules/@apostrophecms/rich-text-widget/index.js +1 -1
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposImageControlDialog.vue +3 -2
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +32 -20
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapAnchor.vue +0 -1
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapLink.vue +0 -1
- package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Image.js +76 -54
- package/modules/@apostrophecms/schema/index.js +1 -1
- package/modules/@apostrophecms/schema/ui/apos/components/AposArrayEditor.vue +0 -1
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputArray.vue +0 -1
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputObject.vue +0 -1
- package/modules/@apostrophecms/schema/ui/apos/components/AposSchema.vue +6 -1
- package/modules/@apostrophecms/widget-type/index.js +4 -0
- package/package.json +4 -4
- package/test/pieces-children/pieces-malformed-child.js +32 -0
- package/test/pieces-malformed.js +33 -0
- package/test/queryBuilders.js +225 -0
- package/test/widgets-children/widgets-malformed-child.js +32 -0
- package/test/widgets-malformed.js +34 -0
|
@@ -20,8 +20,8 @@ jobs:
|
|
|
20
20
|
runs-on: ubuntu-latest
|
|
21
21
|
strategy:
|
|
22
22
|
matrix:
|
|
23
|
-
node-version: [
|
|
24
|
-
mongodb-version: [4.
|
|
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,52 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.48.0 (2023-05-26)
|
|
4
|
+
|
|
5
|
+
### Adds
|
|
6
|
+
|
|
7
|
+
* For performance, add `apos.modules['piece-type']getManagerApiProjection` method to reduce the amount of data returned in the manager
|
|
8
|
+
modal. The projection will contain the fields returned in the method in addition to the existing manager modal
|
|
9
|
+
columns.
|
|
10
|
+
* Add `apos.schema.getRelationshipQueryBuilderChoicesProjection` method to set the projection used in
|
|
11
|
+
`apos.schema.relationshipQueryBuilderChoices`.
|
|
12
|
+
* Rich-text inline images now copies the `alt` attribute from the original image from the Media Library.
|
|
13
|
+
|
|
14
|
+
### Changes
|
|
15
|
+
|
|
16
|
+
* Remove `stripPlaceholderBrs` and `restorePlaceholderBrs` from `AposRichTextWidgetEditor.vue` component.
|
|
17
|
+
* Change tiptap `Gapcursor` display to use a vertical blinking cursor instead of an horizontal cursor, which allow users to add text before and after inline images and tables.
|
|
18
|
+
* You can set `max-width` on `.apos-rich-text-toolbar__inner` to define the width of the rich-text toolbar. It will now
|
|
19
|
+
flow on multiple lines if needed.
|
|
20
|
+
* The `utilityRail` prop of `AposSchema` now defaults to `false`, removing
|
|
21
|
+
the need to explicitly pass it in almost all contexts.
|
|
22
|
+
* Mark `apos.modules['doc-type']` methods `getAutocompleteTitle`, `getAutocompleteProjection` and `autocomplete` as
|
|
23
|
+
deprecated. Our admin UI does not use them, it uses the `autocomplete('...')` query builder.
|
|
24
|
+
More info at https://v3.docs.apostrophecms.org/reference/query-builders.html#autocomplete'.
|
|
25
|
+
* Print a warning with a clear explanation if a module's `index.js` file contains
|
|
26
|
+
no `module.exports` object (often due to a typo), or it is empty.
|
|
27
|
+
|
|
28
|
+
### Fixes
|
|
29
|
+
|
|
30
|
+
* Now errors and exits when a piece-type or widget-type module has a field object with the property `type`. Thanks to [NuktukDev](https://github.com/nuktukdev) for this contribution.
|
|
31
|
+
* Add a default page type value to prevent the dropdown from containing an empty value.
|
|
32
|
+
|
|
33
|
+
## 3.47.0 (2023-05-05)
|
|
34
|
+
|
|
35
|
+
### Changes
|
|
36
|
+
|
|
37
|
+
* Since Node 14 and MongoDB 4.2 have reached their own end-of-support dates,
|
|
38
|
+
we are **no longer supporting them for A3.** Note that our dependency on
|
|
39
|
+
`jsdom` 22 is incompatible with Node 14. Node 16 and Node 18 are both
|
|
40
|
+
still supported. However, because Node 16 reaches its
|
|
41
|
+
end-of-life date quite soon (September), testing and upgrading directly
|
|
42
|
+
to Node 18 is strongly recommended.
|
|
43
|
+
* Updated `sluggo` to version 1.0.0.
|
|
44
|
+
* Updated `jsdom` to version `22.0.0` to address an installation warning about the `word-wrap` module.
|
|
45
|
+
|
|
46
|
+
### Fixes
|
|
47
|
+
|
|
48
|
+
* Fix `extendQueries` to use super pattern for every function in builders and methods (and override properties that are not functions).
|
|
49
|
+
|
|
3
50
|
## 3.46.0 (2023-05-03)
|
|
4
51
|
|
|
5
52
|
### Fixes
|
|
@@ -58,7 +105,11 @@ shouldn't close the link dialog etc.
|
|
|
58
105
|
|
|
59
106
|
### Fixes
|
|
60
107
|
|
|
108
|
+
* 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.
|
|
109
|
+
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
110
|
* Rich text widgets save more reliably when many actions are taken quickly just before save.
|
|
111
|
+
* 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.
|
|
112
|
+
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
113
|
|
|
63
114
|
## 3.44.0 (2023-04-13)
|
|
64
115
|
|
|
@@ -78,11 +129,6 @@ those writing mocha tests of Apostrophe modules.
|
|
|
78
129
|
### Fixes
|
|
79
130
|
* Fix child page slug when title is deleted
|
|
80
131
|
|
|
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
132
|
## 3.43.0 (2023-03-29)
|
|
87
133
|
|
|
88
134
|
### Adds
|
package/lib/moog-require.js
CHANGED
|
@@ -77,6 +77,10 @@ module.exports = function(options) {
|
|
|
77
77
|
}
|
|
78
78
|
if (fs.existsSync(projectLevelPath)) {
|
|
79
79
|
projectLevelDefinition = importFresh(resolveFrom(path.dirname(self.root.filename), projectLevelPath));
|
|
80
|
+
if (Object.keys(projectLevelDefinition).length === 0) {
|
|
81
|
+
/* eslint-disable-next-line no-console */
|
|
82
|
+
console.warn(`⚠️ The file ${projectLevelPath}\ndoes not export anything, did you misspell or forget module.exports?\n`);
|
|
83
|
+
}
|
|
80
84
|
}
|
|
81
85
|
|
|
82
86
|
let relativeTo;
|
|
@@ -26,6 +26,11 @@ module.exports = {
|
|
|
26
26
|
// titles from. The default behavior is to return the `title` property,
|
|
27
27
|
// but since this is a page we are including the slug as well.
|
|
28
28
|
getAutocompleteTitle(doc, query) {
|
|
29
|
+
// TODO Remove in next major version.
|
|
30
|
+
self.apos.util.warnDevOnce(
|
|
31
|
+
'deprecate-get-autocomplete-title',
|
|
32
|
+
'self.getAutocompleteTitle() is deprecated. Use the autocomplete(\'...\') query builder instead. More info at https://v3.docs.apostrophecms.org/reference/query-builders.html#autocomplete'
|
|
33
|
+
);
|
|
29
34
|
return doc.title + ' (' + doc.slug + ')';
|
|
30
35
|
},
|
|
31
36
|
getBrowserData(req) {
|
|
@@ -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
|
-
|
|
476
|
-
|
|
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);
|
|
@@ -506,6 +508,26 @@ module.exports = {
|
|
|
506
508
|
//
|
|
507
509
|
// `query.field` will contain the schema field definition for
|
|
508
510
|
// the relationship the user is attempting to match titles from.
|
|
511
|
+
getRelationshipQueryBuilderChoicesProjection(query) {
|
|
512
|
+
const projection = self.getAutocompleteProjection(query);
|
|
513
|
+
|
|
514
|
+
return {
|
|
515
|
+
...projection,
|
|
516
|
+
title: 1,
|
|
517
|
+
type: 1,
|
|
518
|
+
_id: 1,
|
|
519
|
+
_url: 1,
|
|
520
|
+
slug: 1
|
|
521
|
+
};
|
|
522
|
+
},
|
|
523
|
+
// Returns a MongoDB projection object to be used when querying
|
|
524
|
+
// for this type if all that is needed is a title for display
|
|
525
|
+
// in an autocomplete menu. Default behavior is to
|
|
526
|
+
// return only the `title`, `_id` and `slug` properties.
|
|
527
|
+
// Removing any of these three is not recommended.
|
|
528
|
+
//
|
|
529
|
+
// `query.field` will contain the schema field definition for
|
|
530
|
+
// the relationship the user is attempting to match titles from.
|
|
509
531
|
getAutocompleteProjection(query) {
|
|
510
532
|
return {
|
|
511
533
|
title: 1,
|
|
@@ -522,6 +544,11 @@ module.exports = {
|
|
|
522
544
|
// event start dates and similar information that helps the
|
|
523
545
|
// user distinguish between docs.
|
|
524
546
|
getAutocompleteTitle(doc, query) {
|
|
547
|
+
// TODO Remove in next major version.
|
|
548
|
+
self.apos.util.warnDevOnce(
|
|
549
|
+
'deprecate-get-autocomplete-title',
|
|
550
|
+
'self.getAutocompleteTitle() is deprecated. Use the autocomplete(\'...\') query builder instead. More info at https://v3.docs.apostrophecms.org/reference/query-builders.html#autocomplete'
|
|
551
|
+
);
|
|
525
552
|
return doc.title;
|
|
526
553
|
},
|
|
527
554
|
// Used by `@apostrophecms/version` to label changes that
|
|
@@ -613,6 +640,12 @@ module.exports = {
|
|
|
613
640
|
//
|
|
614
641
|
// We don't launder the input here, see the 'autocomplete' route.
|
|
615
642
|
async autocomplete(req, query) {
|
|
643
|
+
// TODO Remove in next major version.
|
|
644
|
+
self.apos.util.warnDevOnce(
|
|
645
|
+
'deprecate-autocomplete',
|
|
646
|
+
'self.autocomplete() is deprecated. Use the autocomplete(\'...\') query builder instead. More info at https://v3.docs.apostrophecms.org/reference/query-builders.html#autocomplete'
|
|
647
|
+
);
|
|
648
|
+
|
|
616
649
|
const _query = query.find(req, {}).sort('search');
|
|
617
650
|
if (query.extendAutocompleteQuery) {
|
|
618
651
|
query.extendAutocompleteQuery(_query);
|
|
@@ -2322,8 +2355,14 @@ module.exports = {
|
|
|
2322
2355
|
query.and({ $or });
|
|
2323
2356
|
}
|
|
2324
2357
|
}
|
|
2325
|
-
}
|
|
2358
|
+
},
|
|
2326
2359
|
|
|
2360
|
+
viewContext: {
|
|
2361
|
+
def: null,
|
|
2362
|
+
launder(viewContext) {
|
|
2363
|
+
return [ 'manage', 'relationship' ].includes(viewContext) ? viewContext : null;
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2327
2366
|
},
|
|
2328
2367
|
|
|
2329
2368
|
methods: {
|
|
@@ -3073,17 +3112,3 @@ module.exports = {
|
|
|
3073
3112
|
};
|
|
3074
3113
|
}
|
|
3075
3114
|
};
|
|
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
|
+
};
|
|
@@ -462,6 +462,14 @@ module.exports = {
|
|
|
462
462
|
},
|
|
463
463
|
extendMethods(self) {
|
|
464
464
|
return {
|
|
465
|
+
getRelationshipQueryBuilderChoicesProjection(_super, query) {
|
|
466
|
+
const projection = _super(query);
|
|
467
|
+
|
|
468
|
+
return {
|
|
469
|
+
...projection,
|
|
470
|
+
attachment: 1
|
|
471
|
+
};
|
|
472
|
+
},
|
|
465
473
|
getBrowserData(_super, req) {
|
|
466
474
|
const data = _super(req);
|
|
467
475
|
data.components.managerModal = 'AposMediaManager';
|
|
@@ -8,10 +8,14 @@
|
|
|
8
8
|
<div class="apos-input-wrapper">
|
|
9
9
|
<input
|
|
10
10
|
:class="classes"
|
|
11
|
-
v-model="next.url"
|
|
11
|
+
v-model="next.url"
|
|
12
|
+
type="url"
|
|
12
13
|
:placeholder="$t(field.placeholder)"
|
|
13
|
-
:disabled="field.readOnly"
|
|
14
|
-
:
|
|
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.
|
|
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.
|
|
143
|
+
this.tempReadOnly = false;
|
|
136
144
|
}
|
|
137
145
|
}
|
|
138
146
|
}
|
|
@@ -22,6 +22,7 @@ module.exports = {
|
|
|
22
22
|
type: 'select',
|
|
23
23
|
label: 'apostrophe:type',
|
|
24
24
|
required: true,
|
|
25
|
+
def: self.options.apos.page.typeChoices[0].name,
|
|
25
26
|
choices: self.options.apos.page.typeChoices.map(function (type) {
|
|
26
27
|
return {
|
|
27
28
|
value: type.name,
|
|
@@ -297,6 +298,11 @@ module.exports = {
|
|
|
297
298
|
// the `title` property, but since this is a page we are including
|
|
298
299
|
// the slug as well.
|
|
299
300
|
getAutocompleteTitle(doc, query) {
|
|
301
|
+
// TODO Remove in next major version.
|
|
302
|
+
self.apos.util.warnDevOnce(
|
|
303
|
+
'deprecate-get-autocomplete-title',
|
|
304
|
+
'self.getAutocompleteTitle() is deprecated. Use the autocomplete(\'...\') query builder instead. More info at https://v3.docs.apostrophecms.org/reference/query-builders.html#autocomplete'
|
|
305
|
+
);
|
|
300
306
|
return doc.title + ' (' + doc.slug + ')';
|
|
301
307
|
},
|
|
302
308
|
// `req` determines what the user is eligible to edit, `criteria`
|
|
@@ -26,6 +26,20 @@ module.exports = {
|
|
|
26
26
|
// publicApiProjection: {
|
|
27
27
|
// title: 1,
|
|
28
28
|
// _url: 1,
|
|
29
|
+
// },
|
|
30
|
+
// By default the manager modal will get all the pieces fields below + all manager columns
|
|
31
|
+
// you can enable a projection using
|
|
32
|
+
// managerApiProjection: {
|
|
33
|
+
// _id: 1,
|
|
34
|
+
// _url: 1,
|
|
35
|
+
// aposDocId: 1,
|
|
36
|
+
// aposLocale: 1,
|
|
37
|
+
// aposMode: 1,
|
|
38
|
+
// docPermissions: 1,
|
|
39
|
+
// slug: 1,
|
|
40
|
+
// title: 1,
|
|
41
|
+
// type: 1,
|
|
42
|
+
// visibility: 1
|
|
29
43
|
// }
|
|
30
44
|
},
|
|
31
45
|
fields: {
|
|
@@ -182,6 +196,10 @@ module.exports = {
|
|
|
182
196
|
if (!self.options.name) {
|
|
183
197
|
throw new Error('@apostrophecms/pieces require name option');
|
|
184
198
|
}
|
|
199
|
+
const badFieldName = Object.keys(self.fields).indexOf('type') !== -1;
|
|
200
|
+
if (badFieldName) {
|
|
201
|
+
throw new Error(`The ${self.__meta.name} module contains a forbidden field property name: "type".`);
|
|
202
|
+
}
|
|
185
203
|
if (!self.options.label) {
|
|
186
204
|
// Englishify it
|
|
187
205
|
self.options.label = _.startCase(self.options.name);
|
|
@@ -1063,7 +1081,24 @@ module.exports = {
|
|
|
1063
1081
|
return self.apos.permission.can(req, batchOperation.permission, self.name);
|
|
1064
1082
|
}
|
|
1065
1083
|
return true;
|
|
1084
|
+
|
|
1085
|
+
});
|
|
1086
|
+
},
|
|
1087
|
+
getManagerApiProjection(req) {
|
|
1088
|
+
if (!self.options.managerApiProjection) {
|
|
1089
|
+
return null;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
const projection = { ...self.options.managerApiProjection };
|
|
1093
|
+
self.columns.forEach(({ name }) => {
|
|
1094
|
+
const column = (name.startsWith('draft:') || name.startsWith('published:'))
|
|
1095
|
+
? name.replace(/^(draft|published):/, '')
|
|
1096
|
+
: name;
|
|
1097
|
+
|
|
1098
|
+
projection[column] = 1;
|
|
1066
1099
|
});
|
|
1100
|
+
|
|
1101
|
+
return projection;
|
|
1067
1102
|
}
|
|
1068
1103
|
};
|
|
1069
1104
|
},
|
|
@@ -1092,6 +1127,7 @@ module.exports = {
|
|
|
1092
1127
|
editorModal: 'AposDocEditor',
|
|
1093
1128
|
managerModal: 'AposDocsManager'
|
|
1094
1129
|
});
|
|
1130
|
+
browserOptions.managerApiProjection = self.getManagerApiProjection(req);
|
|
1095
1131
|
|
|
1096
1132
|
return browserOptions;
|
|
1097
1133
|
},
|
|
@@ -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: {
|
|
@@ -314,6 +316,10 @@ export default {
|
|
|
314
316
|
const {
|
|
315
317
|
currentPage, pages, results, choices
|
|
316
318
|
} = await this.request({
|
|
319
|
+
...(
|
|
320
|
+
this.moduleOptions.managerApiProjection &&
|
|
321
|
+
{ project: this.moduleOptions.managerApiProjection }
|
|
322
|
+
),
|
|
317
323
|
page: this.currentPage
|
|
318
324
|
});
|
|
319
325
|
|
package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposImageControlDialog.vue
CHANGED
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
:schema="schema"
|
|
17
17
|
:trigger-validation="triggerValidation"
|
|
18
18
|
v-model="docFields"
|
|
19
|
-
:utility-rail="false"
|
|
20
19
|
:modifiers="formModifiers"
|
|
21
20
|
:key="lastSelectionTime"
|
|
22
21
|
:generation="generation"
|
|
@@ -125,11 +124,13 @@ export default {
|
|
|
125
124
|
}
|
|
126
125
|
const image = this.docFields.data._image[0];
|
|
127
126
|
this.docFields.data.imageId = image && image.aposDocId;
|
|
127
|
+
this.docFields.data.alt = image && image.alt;
|
|
128
128
|
this.$emit('before-commands');
|
|
129
129
|
this.editor.commands.setImage({
|
|
130
130
|
imageId: this.docFields.data.imageId,
|
|
131
131
|
caption: this.docFields.data.caption,
|
|
132
|
-
style: this.docFields.data.style
|
|
132
|
+
style: this.docFields.data.style,
|
|
133
|
+
alt: this.docFields.data.alt
|
|
133
134
|
});
|
|
134
135
|
this.close();
|
|
135
136
|
});
|
package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<div>
|
|
3
3
|
<bubble-menu
|
|
4
4
|
class="bubble-menu"
|
|
5
|
-
:tippy-options="{ duration: 100, zIndex: 2000 }"
|
|
5
|
+
:tippy-options="{ maxWidth: 'none', duration: 100, zIndex: 2000 }"
|
|
6
6
|
:editor="editor"
|
|
7
7
|
v-if="editor"
|
|
8
8
|
>
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
</bubble-menu>
|
|
28
28
|
<floating-menu
|
|
29
29
|
class="apos-rich-text-insert-menu" :should-show="showFloatingMenu"
|
|
30
|
-
:editor="editor"
|
|
30
|
+
:editor="editor"
|
|
31
|
+
:tippy-options="{ duration: 100, zIndex: 2000 }"
|
|
31
32
|
v-if="editor"
|
|
32
33
|
>
|
|
33
34
|
<div class="apos-rich-text-insert-menu-heading">
|
|
@@ -178,10 +179,10 @@ export default {
|
|
|
178
179
|
},
|
|
179
180
|
autofocus() {
|
|
180
181
|
// Only true for a new rich text widget
|
|
181
|
-
return !this.
|
|
182
|
+
return !this.value.content.length;
|
|
182
183
|
},
|
|
183
184
|
initialContent() {
|
|
184
|
-
const content = this.transformNamedAnchors(this.
|
|
185
|
+
const content = this.transformNamedAnchors(this.value.content);
|
|
185
186
|
if (content.length) {
|
|
186
187
|
return content;
|
|
187
188
|
}
|
|
@@ -343,26 +344,12 @@ export default {
|
|
|
343
344
|
clearTimeout(this.pending);
|
|
344
345
|
this.pending = null;
|
|
345
346
|
}
|
|
346
|
-
|
|
347
|
-
content = this.restorePlaceholderBrs(content);
|
|
347
|
+
const content = this.editor.getHTML();
|
|
348
348
|
const widget = this.docFields.data;
|
|
349
349
|
widget.content = content;
|
|
350
350
|
// ... removes need for deep watching in parent
|
|
351
351
|
this.$emit('update', { ...widget });
|
|
352
352
|
},
|
|
353
|
-
// Restore placeholder BRs for empty paragraphs. ProseMirror adds these
|
|
354
|
-
// temporarily so the editing experience doesn't break due to contenteditable
|
|
355
|
-
// issues with empty paragraphs, but strips them on save; however
|
|
356
|
-
// seeing them while editing creates a WYSIWYG expectation
|
|
357
|
-
// on the user's part, so we must maintain them
|
|
358
|
-
restorePlaceholderBrs(html) {
|
|
359
|
-
return html.replace(/<(p[^>]*)>(\s*)<\/p>/gi, '<$1><br /></p>');
|
|
360
|
-
},
|
|
361
|
-
// Strip the placeholder BRs again when populating the editor.
|
|
362
|
-
// Otherwise they get doubled by ProseMirror
|
|
363
|
-
stripPlaceholderBrs(html) {
|
|
364
|
-
return html.replace(/<(p[^>]*)>\s*<br \/>\s*<\/p>/gi, '<$1></p>');
|
|
365
|
-
},
|
|
366
353
|
// Legacy content may have `id` and `name` attributes on anchor tags
|
|
367
354
|
// but our tiptap anchor extension needs them on a separate `span`, so nest
|
|
368
355
|
// a span to migrate this content for each relevant anchor tag encountered
|
|
@@ -569,8 +556,10 @@ function traverseNextNode(node) {
|
|
|
569
556
|
|
|
570
557
|
.apos-rich-text-toolbar__inner {
|
|
571
558
|
display: flex;
|
|
559
|
+
flex-wrap: wrap;
|
|
572
560
|
align-items: stretch;
|
|
573
|
-
|
|
561
|
+
max-width: 100%;
|
|
562
|
+
height: auto;
|
|
574
563
|
background-color: var(--a-background-primary);
|
|
575
564
|
color: var(--a-text-primary);
|
|
576
565
|
border-radius: var(--a-border-radius);
|
|
@@ -710,4 +699,27 @@ function traverseNextNode(node) {
|
|
|
710
699
|
.apos-rich-text-insert-menu-heading {
|
|
711
700
|
color: var(--a-base-5);
|
|
712
701
|
}
|
|
702
|
+
|
|
703
|
+
::v-deep .ProseMirror {
|
|
704
|
+
> * + * {
|
|
705
|
+
margin-top: 0.75em;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
> :last-child {
|
|
709
|
+
margin-bottom: 1.75em;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
::v-deep .ProseMirror-gapcursor {
|
|
714
|
+
position: relative;
|
|
715
|
+
display: block;
|
|
716
|
+
height: 20px;
|
|
717
|
+
|
|
718
|
+
&:after {
|
|
719
|
+
width: 1px;
|
|
720
|
+
height: 20px;
|
|
721
|
+
border-left: 1px solid #000;
|
|
722
|
+
border-top: 0 none;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
713
725
|
</style>
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
mergeAttributes,
|
|
3
|
+
Node
|
|
4
|
+
} from '@tiptap/core';
|
|
2
5
|
|
|
3
6
|
export default options => {
|
|
4
7
|
return Node.create({
|
|
5
|
-
|
|
6
8
|
name: 'image',
|
|
7
9
|
|
|
8
10
|
addOptions() {
|
|
@@ -12,22 +14,51 @@ export default options => {
|
|
|
12
14
|
};
|
|
13
15
|
},
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
allowGapCursor: true,
|
|
18
|
+
atom: true,
|
|
19
|
+
selectable: true,
|
|
20
|
+
|
|
21
|
+
group: 'block',
|
|
16
22
|
|
|
17
|
-
|
|
23
|
+
content: 'inline*',
|
|
18
24
|
|
|
19
25
|
draggable: true,
|
|
20
26
|
|
|
27
|
+
isolating: false,
|
|
28
|
+
|
|
21
29
|
addAttributes() {
|
|
22
30
|
return {
|
|
23
31
|
imageId: {
|
|
24
|
-
default: null
|
|
32
|
+
default: null,
|
|
33
|
+
parseHTML: element => {
|
|
34
|
+
const src = element.querySelector('img')?.getAttribute('src');
|
|
35
|
+
|
|
36
|
+
const components = src.split('/');
|
|
37
|
+
if (components.length < 2) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const routeName = components.pop();
|
|
42
|
+
if (routeName !== 'src') {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const imageId = components.pop();
|
|
47
|
+
|
|
48
|
+
return imageId;
|
|
49
|
+
}
|
|
25
50
|
},
|
|
26
51
|
caption: {
|
|
27
|
-
default: ''
|
|
52
|
+
default: '',
|
|
53
|
+
parseHTML: element => element.querySelector('figcaption')?.innerText || ''
|
|
28
54
|
},
|
|
29
55
|
style: {
|
|
30
|
-
default: null
|
|
56
|
+
default: null,
|
|
57
|
+
parseHTML: element => element.getAttribute('class')
|
|
58
|
+
},
|
|
59
|
+
alt: {
|
|
60
|
+
default: null,
|
|
61
|
+
parseHTML: element => element.querySelector('img')?.getAttribute('alt')
|
|
31
62
|
}
|
|
32
63
|
};
|
|
33
64
|
},
|
|
@@ -35,72 +66,63 @@ export default options => {
|
|
|
35
66
|
parseHTML() {
|
|
36
67
|
// <figure>
|
|
37
68
|
// <img src="/media/cc0-images/elephant-660-480.jpg"
|
|
38
|
-
//
|
|
69
|
+
// alt="Elephant at sunset">
|
|
39
70
|
// <figcaption>An elephant at sunset</figcaption>
|
|
40
71
|
// </figure>
|
|
41
72
|
return [
|
|
42
73
|
{
|
|
43
74
|
tag: 'figure',
|
|
44
|
-
|
|
45
|
-
const img = el.querySelector('img');
|
|
46
|
-
const src = img.getAttribute('src');
|
|
47
|
-
if (!img || !src) {
|
|
48
|
-
return false;
|
|
49
|
-
}
|
|
50
|
-
const caption = el.querySelector('figcaption');
|
|
51
|
-
const components = src.split('/');
|
|
52
|
-
if (components.length < 2) {
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
|
-
const routeName = components.pop();
|
|
56
|
-
if (routeName !== 'src') {
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
|
-
const imageId = components.pop();
|
|
60
|
-
const style = el.getAttribute('class');
|
|
61
|
-
if (!imageId) {
|
|
62
|
-
return false;
|
|
63
|
-
}
|
|
64
|
-
const result = {
|
|
65
|
-
imageId,
|
|
66
|
-
style,
|
|
67
|
-
caption: (caption && caption.innerText) || ''
|
|
68
|
-
};
|
|
69
|
-
return result;
|
|
70
|
-
}
|
|
75
|
+
contentElement: 'figcaption'
|
|
71
76
|
}
|
|
72
77
|
];
|
|
73
78
|
},
|
|
74
79
|
|
|
75
|
-
addCommands() {
|
|
76
|
-
return {
|
|
77
|
-
setImage: options => ({ commands }) => {
|
|
78
|
-
return commands.insertContent({
|
|
79
|
-
type: this.name,
|
|
80
|
-
attrs: options
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
|
-
},
|
|
85
|
-
|
|
86
80
|
renderHTML({ HTMLAttributes }) {
|
|
87
81
|
return [
|
|
88
82
|
'figure',
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
},
|
|
92
|
-
[
|
|
93
|
-
'img',
|
|
83
|
+
mergeAttributes(
|
|
84
|
+
this.options.HTMLAttributes,
|
|
94
85
|
{
|
|
95
|
-
|
|
86
|
+
class: HTMLAttributes.style
|
|
96
87
|
}
|
|
88
|
+
),
|
|
89
|
+
[
|
|
90
|
+
'img',
|
|
91
|
+
mergeAttributes(
|
|
92
|
+
HTMLAttributes,
|
|
93
|
+
{
|
|
94
|
+
src: `${apos.modules['@apostrophecms/image'].action}/${HTMLAttributes.imageId}/src`,
|
|
95
|
+
alt: HTMLAttributes.alt,
|
|
96
|
+
draggable: false,
|
|
97
|
+
contenteditable: false
|
|
98
|
+
}
|
|
99
|
+
)
|
|
97
100
|
],
|
|
98
101
|
[
|
|
99
102
|
'figcaption',
|
|
100
|
-
|
|
101
|
-
HTMLAttributes.caption
|
|
103
|
+
0
|
|
102
104
|
]
|
|
103
105
|
];
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
addCommands() {
|
|
109
|
+
return {
|
|
110
|
+
setImage: (attrs) => ({ chain }) => {
|
|
111
|
+
return chain()
|
|
112
|
+
.insertContent({
|
|
113
|
+
type: this.name,
|
|
114
|
+
attrs,
|
|
115
|
+
content: attrs?.caption
|
|
116
|
+
? [ {
|
|
117
|
+
type: 'text',
|
|
118
|
+
text: attrs.caption
|
|
119
|
+
} ]
|
|
120
|
+
: []
|
|
121
|
+
})
|
|
122
|
+
.createParagraphNear()
|
|
123
|
+
.run();
|
|
124
|
+
}
|
|
125
|
+
};
|
|
104
126
|
}
|
|
105
127
|
});
|
|
106
128
|
};
|
|
@@ -1090,7 +1090,7 @@ module.exports = {
|
|
|
1090
1090
|
const idsStorage = field.idsStorage;
|
|
1091
1091
|
const ids = await query.toDistinct(idsStorage);
|
|
1092
1092
|
const manager = self.apos.doc.getManager(field.withType);
|
|
1093
|
-
const relationshipQuery = manager.find(query.req, { aposDocId: { $in: ids } }).project(manager.
|
|
1093
|
+
const relationshipQuery = manager.find(query.req, { aposDocId: { $in: ids } }).project(manager.getRelationshipQueryBuilderChoicesProjection({ field: field }));
|
|
1094
1094
|
if (field.builders) {
|
|
1095
1095
|
relationshipQuery.applyBuilders(field.builders);
|
|
1096
1096
|
}
|
|
@@ -61,7 +61,6 @@
|
|
|
61
61
|
:class="item.open && !alwaysExpand ? 'apos-input-array-inline-item--active' : null"
|
|
62
62
|
v-model="item.schemaInput"
|
|
63
63
|
:trigger-validation="triggerValidation"
|
|
64
|
-
:utility-rail="false"
|
|
65
64
|
:generation="generation"
|
|
66
65
|
:modifiers="['small', 'inverted']"
|
|
67
66
|
:doc-id="docId"
|
|
@@ -103,6 +103,10 @@ module.exports = {
|
|
|
103
103
|
placeholderClass: 'apos-placeholder'
|
|
104
104
|
},
|
|
105
105
|
init(self) {
|
|
106
|
+
const badFieldName = Object.keys(self.fields).indexOf('type') !== -1;
|
|
107
|
+
if (badFieldName) {
|
|
108
|
+
throw new Error(`The ${self.__meta.name} module contains a forbidden field property name: "type".`);
|
|
109
|
+
}
|
|
106
110
|
|
|
107
111
|
self.enableBrowserData();
|
|
108
112
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apostrophe",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.48.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": ">=
|
|
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": "^
|
|
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.
|
|
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,32 @@
|
|
|
1
|
+
const t = require('../../test-lib/test.js');
|
|
2
|
+
|
|
3
|
+
const apiKey = 'this is a test api key';
|
|
4
|
+
|
|
5
|
+
(async function () {
|
|
6
|
+
await t.create({
|
|
7
|
+
root: module,
|
|
8
|
+
|
|
9
|
+
modules: {
|
|
10
|
+
'@apostrophecms/express': {
|
|
11
|
+
options: {
|
|
12
|
+
apiKeys: {
|
|
13
|
+
[apiKey]: {
|
|
14
|
+
role: 'admin'
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
malformed: {
|
|
20
|
+
extend: '@apostrophecms/piece-type',
|
|
21
|
+
fields: {
|
|
22
|
+
add: {
|
|
23
|
+
type: {
|
|
24
|
+
label: 'Foo',
|
|
25
|
+
type: 'string'
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
})();
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const t = require('../test-lib/test.js');
|
|
2
|
+
const { spawn } = require('child_process');
|
|
3
|
+
const assert = require('assert');
|
|
4
|
+
|
|
5
|
+
describe('Malformed Pieces', function () {
|
|
6
|
+
this.timeout(t.timeout);
|
|
7
|
+
it('should fail to initialize with a schema containing a field named "type"', function (done) {
|
|
8
|
+
let throwsError = true;
|
|
9
|
+
const mochaProcess = spawn('node', [ './test/pieces-children/pieces-malformed-child.js' ]);
|
|
10
|
+
|
|
11
|
+
mochaProcess.stdout.on('data', (data) => {
|
|
12
|
+
console.log(`Mocha output: ${data}`);
|
|
13
|
+
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
mochaProcess.stderr.on('data', (data) => {
|
|
17
|
+
const errorMsg = data.toString();
|
|
18
|
+
const errorMatch = errorMsg.match(/(?<error>Error:.*\n)/);
|
|
19
|
+
if (errorMatch) {
|
|
20
|
+
throwsError = true;
|
|
21
|
+
assert.equal(errorMatch.groups.error, 'Error: The malformed module contains a forbidden field property name: "type".\n');
|
|
22
|
+
} else {
|
|
23
|
+
throwsError = false;
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
mochaProcess.on('close', (code) => {
|
|
28
|
+
assert.equal(code, 1, 'Mocha process exited with status code 0');
|
|
29
|
+
assert.ok(throwsError, 'Error message not found in stderr');
|
|
30
|
+
done();
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const t = require('../../test-lib/test.js');
|
|
2
|
+
|
|
3
|
+
const apiKey = 'this is a test api key';
|
|
4
|
+
|
|
5
|
+
(async function () {
|
|
6
|
+
await t.create({
|
|
7
|
+
root: module,
|
|
8
|
+
|
|
9
|
+
modules: {
|
|
10
|
+
'@apostrophecms/express': {
|
|
11
|
+
options: {
|
|
12
|
+
apiKeys: {
|
|
13
|
+
[apiKey]: {
|
|
14
|
+
role: 'admin'
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
malformed: {
|
|
20
|
+
extend: '@apostrophecms/widget-type',
|
|
21
|
+
fields: {
|
|
22
|
+
add: {
|
|
23
|
+
type: {
|
|
24
|
+
label: 'Foo',
|
|
25
|
+
type: 'string'
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
})();
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const t = require('../test-lib/test.js');
|
|
2
|
+
const { spawn } = require('child_process');
|
|
3
|
+
const assert = require('assert');
|
|
4
|
+
|
|
5
|
+
describe('Malformed Widgets', function () {
|
|
6
|
+
this.timeout(t.timeout);
|
|
7
|
+
|
|
8
|
+
it('should fail to initialize with a schema containing a field named "type"', function (done) {
|
|
9
|
+
let throwsError = true;
|
|
10
|
+
const mochaProcess = spawn('node', [ './test/widgets-children/widgets-malformed-child.js' ]);
|
|
11
|
+
|
|
12
|
+
mochaProcess.stdout.on('data', (data) => {
|
|
13
|
+
console.log(`Mocha output: ${data}`);
|
|
14
|
+
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
mochaProcess.stderr.on('data', (data) => {
|
|
18
|
+
const errorMsg = data.toString();
|
|
19
|
+
const errorMatch = errorMsg.match(/(?<error>Error:.*\n)/);
|
|
20
|
+
if (errorMatch) {
|
|
21
|
+
throwsError = true;
|
|
22
|
+
assert.equal(errorMatch.groups.error, 'Error: The malformed module contains a forbidden field property name: "type".\n');
|
|
23
|
+
} else {
|
|
24
|
+
throwsError = false;
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
mochaProcess.on('close', (code) => {
|
|
29
|
+
assert.equal(code, 1, 'Mocha process exited with status code 0');
|
|
30
|
+
assert.ok(throwsError, 'Error message not found in stderr');
|
|
31
|
+
done();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
});
|