apostrophe 3.47.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/CHANGELOG.md +30 -0
- package/lib/moog-require.js +4 -0
- package/modules/@apostrophecms/any-page-type/index.js +5 -0
- package/modules/@apostrophecms/doc-type/index.js +31 -0
- package/modules/@apostrophecms/image/index.js +8 -0
- 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 +4 -0
- 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 +1 -1
- package/test/pieces-children/pieces-malformed-child.js +32 -0
- package/test/pieces-malformed.js +33 -0
- package/test/widgets-children/widgets-malformed-child.js +32 -0
- package/test/widgets-malformed.js +34 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
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
|
+
|
|
3
33
|
## 3.47.0 (2023-05-05)
|
|
4
34
|
|
|
5
35
|
### Changes
|
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) {
|
|
@@ -508,6 +508,26 @@ module.exports = {
|
|
|
508
508
|
//
|
|
509
509
|
// `query.field` will contain the schema field definition for
|
|
510
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.
|
|
511
531
|
getAutocompleteProjection(query) {
|
|
512
532
|
return {
|
|
513
533
|
title: 1,
|
|
@@ -524,6 +544,11 @@ module.exports = {
|
|
|
524
544
|
// event start dates and similar information that helps the
|
|
525
545
|
// user distinguish between docs.
|
|
526
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
|
+
);
|
|
527
552
|
return doc.title;
|
|
528
553
|
},
|
|
529
554
|
// Used by `@apostrophecms/version` to label changes that
|
|
@@ -615,6 +640,12 @@ module.exports = {
|
|
|
615
640
|
//
|
|
616
641
|
// We don't launder the input here, see the 'autocomplete' route.
|
|
617
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
|
+
|
|
618
649
|
const _query = query.find(req, {}).sort('search');
|
|
619
650
|
if (query.extendAutocompleteQuery) {
|
|
620
651
|
query.extendAutocompleteQuery(_query);
|
|
@@ -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';
|
|
@@ -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
|
},
|
|
@@ -316,6 +316,10 @@ export default {
|
|
|
316
316
|
const {
|
|
317
317
|
currentPage, pages, results, choices
|
|
318
318
|
} = await this.request({
|
|
319
|
+
...(
|
|
320
|
+
this.moduleOptions.managerApiProjection &&
|
|
321
|
+
{ project: this.moduleOptions.managerApiProjection }
|
|
322
|
+
),
|
|
319
323
|
page: this.currentPage
|
|
320
324
|
});
|
|
321
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
|
@@ -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,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
|
+
});
|