apostrophe 3.32.0 → 3.34.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 +3 -0
- package/CHANGELOG.md +26 -0
- package/CONTRIBUTING.md +32 -9
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaEditor.vue +27 -3
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +42 -16
- package/modules/@apostrophecms/asset/lib/globalIcons.js +1 -0
- package/modules/@apostrophecms/doc/index.js +9 -0
- package/modules/@apostrophecms/i18n/i18n/en.json +3 -0
- package/modules/@apostrophecms/page/index.js +6 -1
- package/modules/@apostrophecms/piece-type/index.js +38 -0
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +8 -4
- package/modules/@apostrophecms/schema/index.js +4 -1
- package/modules/@apostrophecms/schema/lib/addFieldTypes.js +17 -0
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputArray.vue +292 -12
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputObject.vue +13 -2
- package/modules/@apostrophecms/schema/ui/apos/components/AposSchema.vue +4 -0
- package/modules/@apostrophecms/search/index.js +27 -28
- package/modules/@apostrophecms/submitted-draft/index.js +4 -0
- package/modules/@apostrophecms/ui/ui/apos/components/AposButton.vue +3 -0
- package/modules/@apostrophecms/ui/ui/apos/components/AposSlatList.vue +2 -1
- package/package.json +1 -1
- package/test/concurrent-array-relationships.js +101 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.34.0 (2022-12-12)
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
|
|
7
|
+
* Nested areas work properly in widgets that have the `initialModal: false` property.
|
|
8
|
+
* Apostrophe's search index now properly incorporates most string field types as in A2.
|
|
9
|
+
|
|
10
|
+
### Adds
|
|
11
|
+
|
|
12
|
+
* Relationships load more quickly.
|
|
13
|
+
* Parked page checks at startup are faster.
|
|
14
|
+
|
|
15
|
+
## 3.33.0 (2022-11-28)
|
|
16
|
+
|
|
17
|
+
### Adds
|
|
18
|
+
|
|
19
|
+
* You can now set `inline: true` on schema fields of type `array`. This displays a simple editing interface in the context of the main dialog box for the document in question, avoiding the need to open an additional dialog box. Usually best for cases with just one field or just a few. If your array field has a large number of subfields the default behavior (`inline: false`) is more suitable for your needs. See the [array field](https://v3.docs.apostrophecms.org/reference/field-types/array.html) documentation for more information.
|
|
20
|
+
* Batch feature for publishing pieces.
|
|
21
|
+
* Add extensibility for `rich-text-widget` `defaultOptions`. Every key will now be used in the `AposRichTextWidgetEditor`.
|
|
22
|
+
|
|
23
|
+
### Fixes
|
|
24
|
+
|
|
25
|
+
* Prior to this release, widget templates that contained areas pulled in from related documents would break the ability to add another widget beneath.
|
|
26
|
+
* Validation of object fields now works properly on the browser side, in addition to server-side validation, resolving UX issues.
|
|
27
|
+
* Provisions were added to prevent any possibility of a discrepancy in relationship loading results under high load. It is not clear whether this A2 bug was actually possible in A3.
|
|
28
|
+
|
|
3
29
|
## 3.32.0 (2022-11-09)
|
|
4
30
|
|
|
5
31
|
### Adds
|
package/CONTRIBUTING.md
CHANGED
|
@@ -12,14 +12,17 @@ development updates, and help as you build with Apostrophe. There are also
|
|
|
12
12
|
[Github Discussions](https://github.com/apostrophecms/apostrophe/discussions) and
|
|
13
13
|
[Stack Overflow](https://stackoverflow.com/questions/tagged/apostrophe-cms).
|
|
14
14
|
|
|
15
|
-
- [
|
|
16
|
-
- [
|
|
17
|
-
- [
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
- [
|
|
15
|
+
- [ApostropheCMS Contribution Guide](#apostrophecms-contribution-guide)
|
|
16
|
+
- [Code of Conduct](#code-of-conduct)
|
|
17
|
+
- [How can I contribute?](#how-can-i-contribute)
|
|
18
|
+
- [Reporting Bugs](#reporting-bugs)
|
|
19
|
+
- [Suggesting a Feature or Enhancement](#suggesting-a-feature-or-enhancement)
|
|
20
|
+
- [Fixing Bugs or Submitting Enhancements](#fixing-bugs-or-submitting-enhancements)
|
|
21
|
+
- [Contributing to Apostrophe Core](#contributing-to-apostrophe-core)
|
|
22
|
+
- [**What to expect next**](#what-to-expect-next)
|
|
23
|
+
- [Improving documentation](#improving-documentation)
|
|
24
|
+
- [Share the Love](#share-the-love)
|
|
25
|
+
- [Should I make a new npm module?](#should-i-make-a-new-npm-module)
|
|
23
26
|
|
|
24
27
|
## Code of Conduct
|
|
25
28
|
|
|
@@ -95,6 +98,20 @@ were applicable. For Apostrophe core, that should be in the
|
|
|
95
98
|
and for other modules, add it in their README files (unless the README directs
|
|
96
99
|
you elsewhere).
|
|
97
100
|
|
|
101
|
+
### Contributing to Apostrophe Core
|
|
102
|
+
|
|
103
|
+
This is assuming you are interacting with the ApostropheCMS repositories on the GitHub website. If you are using GitHub Desktop you can read about how to fork a repository in the [GitHub docs.](https://docs.github.com/en/desktop/contributing-and-collaborating-using-github-desktop/adding-and-cloning-repositories/cloning-and-forking-repositories-from-github-desktop)
|
|
104
|
+
|
|
105
|
+
1. Fork the main Apostrophe repository to your own account by clicking on the fork button at the top of the [GitHub page.](https://github.com/apostrophecms/apostrophe) If you are contributing to the latest version of Apostrophe you can simply click on the "Create fork" button on the next screen. If you are contributing to Apostrophe 2, you will need to uncheck the "Copy the main branch only" selection before creating the fork.
|
|
106
|
+
2. The forked version of the repository can be modified in any way you would like without impact on the original repository. As a best practice, we request that you create a branch with a short informative name for making code changes. This makes it easier for our team to track what you are contributing when you make your pull request (PR).
|
|
107
|
+
3. Once you've completed your code changes (and updates to `CHANGELOG.md` if needed - see the notes above) you can push all of your changes to your repo. Navigating to your GitHub repository page you will see a banner for creating a PR. Click on the "Contribute" and then "Open a pull request" buttons.
|
|
108
|
+
4. This will bring up a PR page. There are a number of sections to be filled out. Please read carefully and make selections where needed. Following this checklist is very helpful when our team reviews your request. If you need help, just ask!
|
|
109
|
+
5. Finally, it is best to notify a team member on the [Discord channel](https://discord.com/channels/517772094482677790/701815369005924374) that you have submitted a PR. You can also find our contact addresses on our GitHub pages. We don't get automatic notifications from community-submitted PRs. We will see it in the queue eventually, notifying us will just speed up the process.
|
|
110
|
+
|
|
111
|
+
#### **What to expect next**
|
|
112
|
+
|
|
113
|
+
After we get your PR, we will assign someone from the team to review it. They will follow your testing recommendations and run any tests that you have included. They will also look to make sure your code passes all of our internal linting tests. If there are any issues, the reviewer will highlight the needed changes in their PR review. You can then respond to those suggestions with another round of code changes and submissions to your code branch. Once accepted, the reviewer will merge your changes into the proper repository branch. You can then give yourself a well deserved pat on the back - thanks! 🥳
|
|
114
|
+
|
|
98
115
|
### Improving documentation
|
|
99
116
|
|
|
100
117
|
The [documentation repo](https://github.com/apostrophecms/a3-docs)
|
|
@@ -102,7 +119,13 @@ is public and we appreciate contributions. The core maintainers know Apostrophe
|
|
|
102
119
|
very well, but that can make it hard to see where the gaps in documentation are.
|
|
103
120
|
Please open issues there letting us know (nicely, please). Even better, submit a
|
|
104
121
|
pull request documenting something and you'll be helping many developers going
|
|
105
|
-
forward.
|
|
122
|
+
forward. The mechanism for creating a pull request for documentation is the same as the method described for contributing to the core. To summarize:
|
|
123
|
+
1. Fork
|
|
124
|
+
2. Clone and create branch
|
|
125
|
+
3. Make changes and push to branch
|
|
126
|
+
4. Make pull request (PR) and notify Apostrophe development team
|
|
127
|
+
5. Respond to PR comments with any needed changes
|
|
128
|
+
6. Enjoy your awesome contribution - thanks! 🎉
|
|
106
129
|
|
|
107
130
|
Even typo fixes are great!
|
|
108
131
|
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
:field-id="fieldId"
|
|
49
49
|
:disabled="field && field.readOnly"
|
|
50
50
|
:widget-hovered="hoveredWidget"
|
|
51
|
+
:non-foreign-widget-hovered="hoveredNonForeignWidget"
|
|
51
52
|
:widget-focused="focusedWidget"
|
|
52
53
|
:max-reached="maxReached"
|
|
53
54
|
:rendering="rendering(widget)"
|
|
@@ -135,6 +136,7 @@ export default {
|
|
|
135
136
|
areaId: cuid(),
|
|
136
137
|
next: this.getValidItems(),
|
|
137
138
|
hoveredWidget: null,
|
|
139
|
+
hoveredNonForeignWidget: null,
|
|
138
140
|
focusedWidget: null,
|
|
139
141
|
contextMenuOptions: {
|
|
140
142
|
menu: this.choices
|
|
@@ -239,8 +241,9 @@ export default {
|
|
|
239
241
|
apos.bus.$emit('widget-focus-parent', this.focusedWidget);
|
|
240
242
|
}
|
|
241
243
|
},
|
|
242
|
-
updateWidgetHovered(
|
|
243
|
-
this.hoveredWidget =
|
|
244
|
+
updateWidgetHovered({ _id, nonForeignId }) {
|
|
245
|
+
this.hoveredWidget = _id;
|
|
246
|
+
this.hoveredNonForeignWidget = nonForeignId;
|
|
244
247
|
},
|
|
245
248
|
updateWidgetFocused(widgetId) {
|
|
246
249
|
this.focusedWidget = widgetId;
|
|
@@ -443,9 +446,10 @@ export default {
|
|
|
443
446
|
index
|
|
444
447
|
});
|
|
445
448
|
} else if (!this.widgetHasInitialModal(name)) {
|
|
449
|
+
const widget = this.newWidget(name);
|
|
446
450
|
return this.insert({
|
|
447
451
|
widget: {
|
|
448
|
-
|
|
452
|
+
...widget,
|
|
449
453
|
aposPlaceholder: this.widgetHasPlaceholder(name)
|
|
450
454
|
},
|
|
451
455
|
index
|
|
@@ -558,6 +562,26 @@ export default {
|
|
|
558
562
|
}
|
|
559
563
|
return window.apos.modules[`${item.type}-widget`];
|
|
560
564
|
});
|
|
565
|
+
},
|
|
566
|
+
// Return a new widget object in which defaults are fully populated,
|
|
567
|
+
// especially valid sub-area objects, so that nested edits work on the page
|
|
568
|
+
newWidget(type) {
|
|
569
|
+
const widget = {
|
|
570
|
+
type
|
|
571
|
+
};
|
|
572
|
+
const schema = apos.modules[apos.area.widgetManagers[type]].schema;
|
|
573
|
+
schema.forEach(field => {
|
|
574
|
+
if (field.type === 'area') {
|
|
575
|
+
widget[field.name] = {
|
|
576
|
+
_id: cuid(),
|
|
577
|
+
metaType: 'area',
|
|
578
|
+
items: []
|
|
579
|
+
};
|
|
580
|
+
} else {
|
|
581
|
+
widget[field.name] = field.def ? klona(field.def) : field.def;
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
return widget;
|
|
561
585
|
}
|
|
562
586
|
}
|
|
563
587
|
};
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
:class="{'apos-area-widget-wrapper--foreign': foreign}"
|
|
6
6
|
:data-area-widget="widget._id"
|
|
7
7
|
:data-area-label="widgetLabel"
|
|
8
|
+
:data-apos-widget-foreign="foreign ? 1 : 0"
|
|
9
|
+
:data-apos-widget-id="widget._id"
|
|
8
10
|
>
|
|
9
11
|
<div
|
|
10
12
|
class="apos-area-widget-inner"
|
|
@@ -152,6 +154,10 @@ export default {
|
|
|
152
154
|
type: String,
|
|
153
155
|
default: null
|
|
154
156
|
},
|
|
157
|
+
nonForeignWidgetHovered: {
|
|
158
|
+
type: String,
|
|
159
|
+
default: null
|
|
160
|
+
},
|
|
155
161
|
widgetFocused: {
|
|
156
162
|
type: String,
|
|
157
163
|
default: null
|
|
@@ -262,7 +268,12 @@ export default {
|
|
|
262
268
|
};
|
|
263
269
|
},
|
|
264
270
|
widgetLabel() {
|
|
265
|
-
|
|
271
|
+
const moduleName = `${this.widget.type}-widget`;
|
|
272
|
+
const module = window.apos.modules[moduleName];
|
|
273
|
+
if (!module) {
|
|
274
|
+
console.error(`No ${moduleName} module found for widget type ${this.widget.type}`);
|
|
275
|
+
}
|
|
276
|
+
return module.label;
|
|
266
277
|
},
|
|
267
278
|
widgetOptions() {
|
|
268
279
|
return this.widgets[this.widget.type];
|
|
@@ -283,11 +294,13 @@ export default {
|
|
|
283
294
|
return false;
|
|
284
295
|
}
|
|
285
296
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
297
|
+
return !(this.hovered || this.nonForeignHovered);
|
|
298
|
+
},
|
|
299
|
+
hovered() {
|
|
300
|
+
return this.widgetHovered === this.widget._id;
|
|
301
|
+
},
|
|
302
|
+
nonForeignHovered() {
|
|
303
|
+
return this.nonForeignWidgetHovered === this.widget._id;
|
|
291
304
|
},
|
|
292
305
|
// Sets up all the interaction classes based on the current
|
|
293
306
|
// state. If our widget is suppressed, return a blank UI state and reset
|
|
@@ -297,11 +310,11 @@ export default {
|
|
|
297
310
|
controls: this.state.controls.show ? this.classes.show : null,
|
|
298
311
|
labels: this.state.labels.show ? this.classes.show : null,
|
|
299
312
|
container: this.state.container.focus ? this.classes.focus
|
|
300
|
-
: (this.state.container.highlight ? this.classes.highlight : null),
|
|
313
|
+
: ((this.state.container.highlight || this.nonForeignHovered) ? this.classes.highlight : null),
|
|
301
314
|
addTop: this.state.add.top.focus ? this.classes.focus
|
|
302
|
-
: (this.state.add.top.show ? this.classes.show : null),
|
|
315
|
+
: ((this.state.add.top.show || this.nonForeignHovered) ? this.classes.show : null),
|
|
303
316
|
addBottom: this.state.add.bottom.focus ? this.classes.focus
|
|
304
|
-
: (this.state.add.bottom.show ? this.classes.show : null)
|
|
317
|
+
: ((this.state.add.bottom.show || this.nonForeignHovered) ? this.classes.show : null)
|
|
305
318
|
};
|
|
306
319
|
|
|
307
320
|
if (this.isSuppressed) {
|
|
@@ -356,9 +369,7 @@ export default {
|
|
|
356
369
|
|
|
357
370
|
this.breadcrumbs.$lastEl = this.$el;
|
|
358
371
|
|
|
359
|
-
|
|
360
|
-
this.getBreadcrumbs();
|
|
361
|
-
}
|
|
372
|
+
this.getBreadcrumbs();
|
|
362
373
|
|
|
363
374
|
if (this.widgetFocused) {
|
|
364
375
|
// If another widget was in focus (because the user clicked the "add"
|
|
@@ -395,11 +406,18 @@ export default {
|
|
|
395
406
|
if (this.focused) {
|
|
396
407
|
return;
|
|
397
408
|
}
|
|
398
|
-
this.
|
|
399
|
-
|
|
409
|
+
if (!this.foreign) {
|
|
410
|
+
this.state.add.top.show = true;
|
|
411
|
+
this.state.add.bottom.show = true;
|
|
412
|
+
}
|
|
400
413
|
this.state.container.highlight = true;
|
|
401
414
|
this.state.labels.show = true;
|
|
402
|
-
|
|
415
|
+
const closest = this.foreign && this.$el.closest('[data-apos-widget-foreign="0"]');
|
|
416
|
+
const closestId = closest && closest.getAttribute('data-apos-widget-id');
|
|
417
|
+
apos.bus.$emit('widget-hover', {
|
|
418
|
+
_id: this.widget._id,
|
|
419
|
+
nonForeignId: this.foreign ? closestId : null
|
|
420
|
+
});
|
|
403
421
|
},
|
|
404
422
|
|
|
405
423
|
mouseleave() {
|
|
@@ -412,6 +430,12 @@ export default {
|
|
|
412
430
|
this.state.add.top.show = false;
|
|
413
431
|
this.state.add.bottom.show = false;
|
|
414
432
|
}
|
|
433
|
+
if (this.hovered) {
|
|
434
|
+
apos.bus.$emit('widget-hover', {
|
|
435
|
+
_id: null,
|
|
436
|
+
nonForeignId: null
|
|
437
|
+
});
|
|
438
|
+
}
|
|
415
439
|
},
|
|
416
440
|
|
|
417
441
|
focus(e) {
|
|
@@ -432,7 +456,7 @@ export default {
|
|
|
432
456
|
this.focused = false;
|
|
433
457
|
this.resetState();
|
|
434
458
|
this.highlightable = false;
|
|
435
|
-
document.removeEventListener('click', this.
|
|
459
|
+
document.removeEventListener('click', this.unfocus);
|
|
436
460
|
apos.bus.$emit('widget-focus', null);
|
|
437
461
|
}
|
|
438
462
|
},
|
|
@@ -477,9 +501,11 @@ export default {
|
|
|
477
501
|
widgetComponent(type) {
|
|
478
502
|
return this.moduleOptions.components.widgets[type];
|
|
479
503
|
},
|
|
504
|
+
|
|
480
505
|
widgetEditorComponent(type) {
|
|
481
506
|
return this.moduleOptions.components.widgetEditors[type];
|
|
482
507
|
}
|
|
508
|
+
|
|
483
509
|
}
|
|
484
510
|
};
|
|
485
511
|
</script>
|
|
@@ -80,6 +80,7 @@ module.exports = {
|
|
|
80
80
|
'trash-can-outline-icon': 'TrashCanOutline',
|
|
81
81
|
'tune-icon': 'Tune',
|
|
82
82
|
'undo-icon': 'UndoVariant',
|
|
83
|
+
'unfold-less-horizontal-icon': 'UnfoldLessHorizontal',
|
|
83
84
|
'unfold-more-horizontal-icon': 'UnfoldMoreHorizontal',
|
|
84
85
|
'video-icon': 'Video',
|
|
85
86
|
'view-column-icon': 'ViewColumn',
|
|
@@ -347,6 +347,15 @@ module.exports = {
|
|
|
347
347
|
submitted: 1,
|
|
348
348
|
aposLocale: 1
|
|
349
349
|
});
|
|
350
|
+
await self.db.createIndex({
|
|
351
|
+
type: 1,
|
|
352
|
+
aposDocId: 1,
|
|
353
|
+
aposLocale: 1
|
|
354
|
+
});
|
|
355
|
+
await self.db.createIndex({
|
|
356
|
+
aposDocId: 1,
|
|
357
|
+
aposLocale: 1
|
|
358
|
+
});
|
|
350
359
|
await self.createPathLevelIndex();
|
|
351
360
|
},
|
|
352
361
|
async createTextIndex() {
|
|
@@ -306,6 +306,9 @@
|
|
|
306
306
|
"publish": "Publish",
|
|
307
307
|
"publishBeforeUsingTooltip": "Publish this content before using it in a relationship",
|
|
308
308
|
"published": "Published",
|
|
309
|
+
"publishType": "Publish {{ type }}",
|
|
310
|
+
"publishingBatchConfirmation": "Are you sure you want to publish {{ count }} {{ type }}?",
|
|
311
|
+
"publishingBatchConfirmationButton": "Yes, publish content.",
|
|
309
312
|
"rawCssAndJs": "Raw CSS and JS",
|
|
310
313
|
"rawHtml": "Raw HTML",
|
|
311
314
|
"rawHtmlCode": "Raw HTML (Code)",
|
|
@@ -1867,7 +1867,12 @@ database.`);
|
|
|
1867
1867
|
} else {
|
|
1868
1868
|
parentSlug = item.parent;
|
|
1869
1869
|
}
|
|
1870
|
-
return self.findOneForEditing(req, {
|
|
1870
|
+
return self.findOneForEditing(req, {
|
|
1871
|
+
slug: parentSlug
|
|
1872
|
+
}, {
|
|
1873
|
+
areas: false,
|
|
1874
|
+
relationships: false
|
|
1875
|
+
});
|
|
1871
1876
|
}
|
|
1872
1877
|
async function findExisting() {
|
|
1873
1878
|
return self.findOneForEditing(req, { parkedId: item.parkedId });
|
|
@@ -105,6 +105,19 @@ module.exports = {
|
|
|
105
105
|
utilityOperations: {},
|
|
106
106
|
batchOperations: {
|
|
107
107
|
add: {
|
|
108
|
+
publish: {
|
|
109
|
+
label: 'apostrophe:publish',
|
|
110
|
+
messages: {
|
|
111
|
+
progress: 'Publishing {{ type }}...',
|
|
112
|
+
completed: 'Published {{ count }} {{ type }}.'
|
|
113
|
+
},
|
|
114
|
+
icon: 'earth-icon',
|
|
115
|
+
modalOptions: {
|
|
116
|
+
title: 'apostrophe:publishType',
|
|
117
|
+
description: 'apostrophe:publishingBatchConfirmation',
|
|
118
|
+
confirmationButton: 'apostrophe:publishingBatchConfirmationButton'
|
|
119
|
+
}
|
|
120
|
+
},
|
|
108
121
|
archive: {
|
|
109
122
|
label: 'apostrophe:archive',
|
|
110
123
|
messages: {
|
|
@@ -295,6 +308,31 @@ module.exports = {
|
|
|
295
308
|
}
|
|
296
309
|
return self.publish(req, draft);
|
|
297
310
|
},
|
|
311
|
+
async publish (req) {
|
|
312
|
+
if (!Array.isArray(req.body._ids)) {
|
|
313
|
+
throw self.apos.error('invalid');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
req.body._ids = req.body._ids.map(_id => {
|
|
317
|
+
return self.inferIdLocaleAndMode(req, _id);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
return self.apos.modules['@apostrophecms/job'].runBatch(
|
|
321
|
+
req,
|
|
322
|
+
req.body._ids,
|
|
323
|
+
async function(req, id) {
|
|
324
|
+
const piece = await self.findOneForEditing(req, { _id: id });
|
|
325
|
+
|
|
326
|
+
if (!piece) {
|
|
327
|
+
throw self.apos.error('notfound');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
await self.publish(req, piece);
|
|
331
|
+
}, {
|
|
332
|
+
action: 'publish'
|
|
333
|
+
}
|
|
334
|
+
);
|
|
335
|
+
},
|
|
298
336
|
async archive (req) {
|
|
299
337
|
if (!Array.isArray(req.body._ids)) {
|
|
300
338
|
throw self.apos.error('invalid');
|
package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue
CHANGED
|
@@ -107,16 +107,20 @@ export default {
|
|
|
107
107
|
editorOptions() {
|
|
108
108
|
const activeOptions = Object.assign({}, this.options);
|
|
109
109
|
|
|
110
|
-
// Allow toolbar option to pass through if `false`
|
|
111
|
-
activeOptions.toolbar = (activeOptions.toolbar !== undefined)
|
|
112
|
-
? activeOptions.toolbar : this.defaultOptions.toolbar;
|
|
113
|
-
|
|
114
110
|
activeOptions.styles = this.enhanceStyles(
|
|
115
111
|
activeOptions.styles?.length
|
|
116
112
|
? activeOptions.styles
|
|
117
113
|
: this.defaultOptions.styles
|
|
118
114
|
);
|
|
119
115
|
|
|
116
|
+
// Allow default options to pass through if `false`
|
|
117
|
+
Object.keys(this.defaultOptions).forEach((option) => {
|
|
118
|
+
if (option !== 'styles') {
|
|
119
|
+
activeOptions[option] = (activeOptions[option] !== undefined)
|
|
120
|
+
? activeOptions[option] : this.defaultOptions[option];
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
120
124
|
activeOptions.className = (activeOptions.className !== undefined)
|
|
121
125
|
? activeOptions.className : this.moduleOptions.className;
|
|
122
126
|
|
|
@@ -661,9 +661,12 @@ module.exports = {
|
|
|
661
661
|
let relationships = [];
|
|
662
662
|
|
|
663
663
|
function findRelationships(schema, arrays) {
|
|
664
|
+
// Shallow clone of each relationship to allow
|
|
665
|
+
// for independent _dotPath and _arrays properties
|
|
666
|
+
// for different requests
|
|
664
667
|
const _relationships = _.filter(schema, function (field) {
|
|
665
668
|
return !!self.fieldTypes[field.type].relate;
|
|
666
|
-
});
|
|
669
|
+
}).map(relationship => ({ ...relationship }));
|
|
667
670
|
_.each(_relationships, function (relationship) {
|
|
668
671
|
if (!arrays.length) {
|
|
669
672
|
relationship._dotPath = relationship.name;
|
|
@@ -78,6 +78,18 @@ module.exports = (self) => {
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
|
+
},
|
|
82
|
+
index: function (value, field, texts) {
|
|
83
|
+
for (const item of ((value && value.items) || [])) {
|
|
84
|
+
const manager = item.type && self.apos.area.getWidgetManager(item.type);
|
|
85
|
+
if (!manager) {
|
|
86
|
+
self.apos.area.warnMissingWidgetType(item.type);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (manager.addSearchTexts) {
|
|
90
|
+
manager.addSearchTexts(item, texts);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
81
93
|
}
|
|
82
94
|
});
|
|
83
95
|
|
|
@@ -845,6 +857,11 @@ module.exports = (self) => {
|
|
|
845
857
|
}
|
|
846
858
|
return self.isEqual(req, field.schema, one[field.name], two[field.name]);
|
|
847
859
|
},
|
|
860
|
+
index: function (value, field, texts) {
|
|
861
|
+
if (value) {
|
|
862
|
+
self.apos.schema.indexFields(field.schema, value, texts);
|
|
863
|
+
}
|
|
864
|
+
},
|
|
848
865
|
def: {}
|
|
849
866
|
});
|
|
850
867
|
|
|
@@ -11,7 +11,85 @@
|
|
|
11
11
|
/>
|
|
12
12
|
</template>
|
|
13
13
|
<template #body>
|
|
14
|
-
<div
|
|
14
|
+
<div v-if="field.inline">
|
|
15
|
+
<draggable
|
|
16
|
+
v-if="field.inline"
|
|
17
|
+
class="apos-input-array-inline"
|
|
18
|
+
tag="div"
|
|
19
|
+
role="list"
|
|
20
|
+
:list="items"
|
|
21
|
+
v-bind="dragOptions"
|
|
22
|
+
:id="listId"
|
|
23
|
+
>
|
|
24
|
+
<div
|
|
25
|
+
v-for="(item, index) in items"
|
|
26
|
+
:key="item._id"
|
|
27
|
+
class="apos-input-array-inline-item"
|
|
28
|
+
:class="item.open ? 'apos-input-array-inline-item--active' : null"
|
|
29
|
+
>
|
|
30
|
+
<div class="apos-input-array-inline-item-controls">
|
|
31
|
+
<AposIndicator
|
|
32
|
+
icon="drag-icon"
|
|
33
|
+
class="apos-drag-handle"
|
|
34
|
+
/>
|
|
35
|
+
<AposButton
|
|
36
|
+
v-if="item.open && !alwaysExpand"
|
|
37
|
+
class="apos-input-array-inline-collapse"
|
|
38
|
+
:icon-size="20"
|
|
39
|
+
label="apostrophe:close"
|
|
40
|
+
icon="unfold-less-horizontal-icon"
|
|
41
|
+
type="subtle"
|
|
42
|
+
:modifiers="['inline', 'no-motion']"
|
|
43
|
+
:icon-only="true"
|
|
44
|
+
@click="closeInlineItem(item._id)"
|
|
45
|
+
/>
|
|
46
|
+
</div>
|
|
47
|
+
<div class="apos-input-array-inline-content-wrapper">
|
|
48
|
+
<h3
|
|
49
|
+
class="apos-input-array-inline-label"
|
|
50
|
+
v-if="!item.open && !alwaysExpand"
|
|
51
|
+
@click="openInlineItem(item._id)"
|
|
52
|
+
>
|
|
53
|
+
{{ getLabel(item._id, index) }}
|
|
54
|
+
</h3>
|
|
55
|
+
<transition name="collapse">
|
|
56
|
+
<div
|
|
57
|
+
v-show="item.open"
|
|
58
|
+
class="apos-input-array-inline-schema-wrapper"
|
|
59
|
+
>
|
|
60
|
+
<AposSchema
|
|
61
|
+
:schema="field.schema"
|
|
62
|
+
v-model="item.schemaInput"
|
|
63
|
+
:trigger-validation="triggerValidation"
|
|
64
|
+
:utility-rail="false"
|
|
65
|
+
:generation="generation"
|
|
66
|
+
:modifiers="['small', 'inverted']"
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
</transition>
|
|
70
|
+
</div>
|
|
71
|
+
<div class="apos-input-array-inline-item-controls apos-input-array-inline-item-controls--remove">
|
|
72
|
+
<AposButton
|
|
73
|
+
label="apostrophe:removeItem"
|
|
74
|
+
icon="trash-can-outline-icon"
|
|
75
|
+
type="subtle"
|
|
76
|
+
:modifiers="['inline', 'danger', 'no-motion']"
|
|
77
|
+
:icon-only="true"
|
|
78
|
+
@click="remove(item._id)"
|
|
79
|
+
/>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</draggable>
|
|
83
|
+
<AposButton
|
|
84
|
+
type="button"
|
|
85
|
+
label="apostrophe:addItem"
|
|
86
|
+
icon="plus-icon"
|
|
87
|
+
:disabled="disableAdd()"
|
|
88
|
+
:modifiers="[ 'block' ]"
|
|
89
|
+
@click="add"
|
|
90
|
+
/>
|
|
91
|
+
</div>
|
|
92
|
+
<div v-else class="apos-input-array">
|
|
15
93
|
<label class="apos-input-wrapper">
|
|
16
94
|
<AposButton
|
|
17
95
|
:label="editLabel"
|
|
@@ -27,27 +105,100 @@
|
|
|
27
105
|
|
|
28
106
|
<script>
|
|
29
107
|
import AposInputMixin from 'Modules/@apostrophecms/schema/mixins/AposInputMixin.js';
|
|
108
|
+
import cuid from 'cuid';
|
|
109
|
+
import { klona } from 'klona';
|
|
110
|
+
import draggable from 'vuedraggable';
|
|
30
111
|
|
|
31
112
|
export default {
|
|
32
113
|
name: 'AposInputArray',
|
|
114
|
+
components: { draggable },
|
|
33
115
|
mixins: [ AposInputMixin ],
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
116
|
+
props: {
|
|
117
|
+
generation: {
|
|
118
|
+
type: Number,
|
|
119
|
+
required: false,
|
|
120
|
+
default: null
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
data() {
|
|
124
|
+
const next = this.getNext();
|
|
125
|
+
const data = {
|
|
126
|
+
next,
|
|
127
|
+
items: modelItems(next, this.field)
|
|
39
128
|
};
|
|
129
|
+
return data;
|
|
40
130
|
},
|
|
41
131
|
computed: {
|
|
42
|
-
|
|
132
|
+
alwaysExpand() {
|
|
133
|
+
return alwaysExpand(this.field);
|
|
134
|
+
},
|
|
135
|
+
listId() {
|
|
136
|
+
return `sortableList-${cuid()}`;
|
|
137
|
+
},
|
|
138
|
+
dragOptions() {
|
|
139
|
+
return {
|
|
140
|
+
disabled: this.field.readOnly || this.next.length <= 1,
|
|
141
|
+
ghostClass: 'apos-is-dragging',
|
|
142
|
+
handle: '.apos-drag-handle'
|
|
143
|
+
};
|
|
144
|
+
},
|
|
145
|
+
editLabel() {
|
|
43
146
|
return {
|
|
44
147
|
key: 'apostrophe:editType',
|
|
45
148
|
type: this.$t(this.field.label)
|
|
46
149
|
};
|
|
150
|
+
},
|
|
151
|
+
effectiveError() {
|
|
152
|
+
const error = this.error || this.serverError;
|
|
153
|
+
// Server-side errors behave differently
|
|
154
|
+
const name = error?.name || error;
|
|
155
|
+
if (name === 'invalid') {
|
|
156
|
+
// Always due to a subproperty which will display its own error,
|
|
157
|
+
// don't confuse the user
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
return error;
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
watch: {
|
|
164
|
+
generation() {
|
|
165
|
+
this.next = this.getNext();
|
|
166
|
+
this.items = modelItems(this.next, this.field);
|
|
167
|
+
},
|
|
168
|
+
items: {
|
|
169
|
+
deep: true,
|
|
170
|
+
handler() {
|
|
171
|
+
const erroneous = this.items.filter(item => item.schemaInput.hasErrors);
|
|
172
|
+
if (erroneous.length) {
|
|
173
|
+
erroneous.forEach(item => {
|
|
174
|
+
if (!item.open) {
|
|
175
|
+
// Make errors visible
|
|
176
|
+
item.open = true;
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
} else {
|
|
180
|
+
const next = this.items.map(item => ({
|
|
181
|
+
...item.schemaInput.data,
|
|
182
|
+
_id: item._id,
|
|
183
|
+
metaType: 'arrayItem',
|
|
184
|
+
scopedArrayName: this.field.scopedArrayName
|
|
185
|
+
}));
|
|
186
|
+
this.next = next;
|
|
187
|
+
}
|
|
188
|
+
// Our validate method was called first before that of
|
|
189
|
+
// the subfields, so remedy that by calling again on any
|
|
190
|
+
// change to the subfield state during validation
|
|
191
|
+
if (this.triggerValidation) {
|
|
192
|
+
this.validateAndEmit();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
47
195
|
}
|
|
48
196
|
},
|
|
49
197
|
methods: {
|
|
50
|
-
validate
|
|
198
|
+
validate(value) {
|
|
199
|
+
if (this.items.find(item => item.schemaInput.hasErrors)) {
|
|
200
|
+
return 'invalid';
|
|
201
|
+
}
|
|
51
202
|
if (this.field.required && !value.length) {
|
|
52
203
|
return 'required';
|
|
53
204
|
}
|
|
@@ -59,10 +210,7 @@ export default {
|
|
|
59
210
|
}
|
|
60
211
|
return false;
|
|
61
212
|
},
|
|
62
|
-
|
|
63
|
-
this.next = items;
|
|
64
|
-
},
|
|
65
|
-
async edit () {
|
|
213
|
+
async edit() {
|
|
66
214
|
const result = await apos.modal.execute('AposArrayEditor', {
|
|
67
215
|
field: this.field,
|
|
68
216
|
items: this.next,
|
|
@@ -71,7 +219,139 @@ export default {
|
|
|
71
219
|
if (result) {
|
|
72
220
|
this.next = result;
|
|
73
221
|
}
|
|
222
|
+
},
|
|
223
|
+
getNext() {
|
|
224
|
+
// Next should consistently be an array.
|
|
225
|
+
return (this.value && Array.isArray(this.value.data))
|
|
226
|
+
? this.value.data : (this.field.def || []);
|
|
227
|
+
},
|
|
228
|
+
disableAdd() {
|
|
229
|
+
return this.field.max && (this.items.length >= this.field.max);
|
|
230
|
+
},
|
|
231
|
+
remove(_id) {
|
|
232
|
+
this.items = this.items.filter(item => item._id !== _id);
|
|
233
|
+
},
|
|
234
|
+
add() {
|
|
235
|
+
const _id = cuid();
|
|
236
|
+
this.items.push({
|
|
237
|
+
_id,
|
|
238
|
+
schemaInput: {
|
|
239
|
+
data: this.newInstance()
|
|
240
|
+
},
|
|
241
|
+
open: alwaysExpand(this.field)
|
|
242
|
+
});
|
|
243
|
+
this.openInlineItem(_id);
|
|
244
|
+
},
|
|
245
|
+
newInstance() {
|
|
246
|
+
const instance = {};
|
|
247
|
+
for (const field of this.field.schema) {
|
|
248
|
+
if (field.def !== undefined) {
|
|
249
|
+
instance[field.name] = klona(field.def);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return instance;
|
|
253
|
+
},
|
|
254
|
+
getLabel(id, index) {
|
|
255
|
+
const titleField = this.field.titleField || null;
|
|
256
|
+
const item = this.items.find(item => item._id === id);
|
|
257
|
+
return item.schemaInput.data[titleField] || `Item ${index + 1}`;
|
|
258
|
+
},
|
|
259
|
+
openInlineItem(id) {
|
|
260
|
+
this.items.forEach(item => {
|
|
261
|
+
item.open = (item._id === id) || this.alwaysExpand;
|
|
262
|
+
});
|
|
263
|
+
},
|
|
264
|
+
closeInlineItem(id) {
|
|
265
|
+
this.items.forEach(item => {
|
|
266
|
+
item.open = this.alwaysExpand;
|
|
267
|
+
});
|
|
74
268
|
}
|
|
75
269
|
}
|
|
76
270
|
};
|
|
271
|
+
|
|
272
|
+
function modelItems(items, field) {
|
|
273
|
+
return items.map(item => {
|
|
274
|
+
const open = alwaysExpand(field);
|
|
275
|
+
return {
|
|
276
|
+
_id: item._id || cuid(),
|
|
277
|
+
schemaInput: {
|
|
278
|
+
data: item
|
|
279
|
+
},
|
|
280
|
+
open
|
|
281
|
+
};
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function alwaysExpand(field) {
|
|
286
|
+
if (!field.inline) {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
if (field.inline.alwaysExpand === undefined) {
|
|
290
|
+
return field.schema.length < 3;
|
|
291
|
+
}
|
|
292
|
+
return field.inline.alwaysExpand;
|
|
293
|
+
}
|
|
77
294
|
</script>
|
|
295
|
+
<style lang="scss" scoped>
|
|
296
|
+
.apos-is-dragging {
|
|
297
|
+
opacity: 0.5;
|
|
298
|
+
background: var(--a-base-4);
|
|
299
|
+
}
|
|
300
|
+
.apos-input-array-inline-label {
|
|
301
|
+
transition: background-color 0.3s ease;
|
|
302
|
+
@include type-label;
|
|
303
|
+
margin: 0;
|
|
304
|
+
&:hover {
|
|
305
|
+
cursor: pointer;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
.apos-input-array-inline-item {
|
|
309
|
+
position: relative;
|
|
310
|
+
transition: background-color 0.3s ease;
|
|
311
|
+
display: flex;
|
|
312
|
+
border-bottom: 1px solid var(--a-base-9);
|
|
313
|
+
&:hover {
|
|
314
|
+
background-color: var(--a-base-10);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
.apos-input-array-inline-collapse {
|
|
318
|
+
position: absolute;
|
|
319
|
+
top: $spacing-quadruple;
|
|
320
|
+
left: 7.5px;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.apos-input-array-inline-item--active {
|
|
324
|
+
background-color: var(--a-base-10);
|
|
325
|
+
border-bottom: 1px solid var(--a-base-6);
|
|
326
|
+
.apos-input-array-inline-content-wrapper {
|
|
327
|
+
padding-top: $spacing-base;
|
|
328
|
+
padding-bottom: $spacing-base;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
.apos-input-array-inline-item-controls {
|
|
332
|
+
padding: $spacing-base;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.apos-input-array-inline-label {
|
|
336
|
+
padding-top: $spacing-base;
|
|
337
|
+
padding-bottom: $spacing-base;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.apos-input-array-inline-content-wrapper {
|
|
341
|
+
flex-grow: 1;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.apos-input-array-inline-schema-wrapper {
|
|
345
|
+
max-height: 999px;
|
|
346
|
+
overflow: hidden;
|
|
347
|
+
transition: max-height 0.5s;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.collapse-enter, .collapse-leave-to {
|
|
351
|
+
max-height: 0;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.collapse-enter-to, .collapse-leave {
|
|
355
|
+
max-height: 999px;
|
|
356
|
+
}
|
|
357
|
+
</style>
|
|
@@ -49,8 +49,19 @@ export default {
|
|
|
49
49
|
};
|
|
50
50
|
},
|
|
51
51
|
watch: {
|
|
52
|
-
schemaInput
|
|
53
|
-
|
|
52
|
+
schemaInput: {
|
|
53
|
+
deep: true,
|
|
54
|
+
handler() {
|
|
55
|
+
if (!this.schemaInput.hasErrors) {
|
|
56
|
+
this.next = this.schemaInput.data;
|
|
57
|
+
}
|
|
58
|
+
// Our validate method was called first before that of
|
|
59
|
+
// the subfields, so remedy that by calling again on any
|
|
60
|
+
// change to the subfield state during validation
|
|
61
|
+
if (this.triggerValidation) {
|
|
62
|
+
this.validateAndEmit();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
54
65
|
},
|
|
55
66
|
generation() {
|
|
56
67
|
this.next = this.getNext();
|
|
@@ -330,10 +330,14 @@ export default {
|
|
|
330
330
|
.apos-field {
|
|
331
331
|
.apos-schema ::v-deep & {
|
|
332
332
|
margin-bottom: $spacing-quadruple;
|
|
333
|
+
&.apos-field--small,
|
|
333
334
|
&.apos-field--micro,
|
|
334
335
|
&.apos-field--margin-micro {
|
|
335
336
|
margin-bottom: $spacing-double;
|
|
336
337
|
}
|
|
338
|
+
&.apos-field--margin-none {
|
|
339
|
+
margin-bottom: 0;
|
|
340
|
+
}
|
|
337
341
|
}
|
|
338
342
|
|
|
339
343
|
.apos-schema ::v-deep .apos-toolbar & {
|
|
@@ -68,6 +68,7 @@ module.exports = {
|
|
|
68
68
|
self.options.suggestions.url = self.options.suggestions.url || self.action + '/suggest';
|
|
69
69
|
self.dispatchAll();
|
|
70
70
|
self.enableFilters();
|
|
71
|
+
self.addMigrations();
|
|
71
72
|
},
|
|
72
73
|
routes(self) {
|
|
73
74
|
return {
|
|
@@ -119,6 +120,17 @@ module.exports = {
|
|
|
119
120
|
}
|
|
120
121
|
},
|
|
121
122
|
|
|
123
|
+
addMigrations() {
|
|
124
|
+
self.addIndexFixMigration();
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
addIndexFixMigration() {
|
|
128
|
+
// Search index lacked most text fields, correct that with a one-time migration
|
|
129
|
+
self.apos.migration.add('search-index-fix', async () => {
|
|
130
|
+
return self.indexTask();
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
|
|
122
134
|
suggest(req, q) {
|
|
123
135
|
return self.apos.doc.find(req).limit(self.options.suggestions && (self.options.suggestions.limit || 10)).search(q).project({
|
|
124
136
|
_url: 1,
|
|
@@ -235,7 +247,6 @@ module.exports = {
|
|
|
235
247
|
indexDoc(req, doc) {
|
|
236
248
|
|
|
237
249
|
const texts = self.getSearchTexts(doc);
|
|
238
|
-
|
|
239
250
|
_.each(texts, function (text) {
|
|
240
251
|
if (text.text === undefined) {
|
|
241
252
|
text.text = '';
|
|
@@ -316,29 +327,11 @@ module.exports = {
|
|
|
316
327
|
text: doc.slug,
|
|
317
328
|
silent: true
|
|
318
329
|
});
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
// snippets in a snippet widget. Allow those items to be found
|
|
325
|
-
// on their own as search results, and avoid bloating the
|
|
326
|
-
// search text up to the 16MB limit
|
|
327
|
-
if (dotPath.match(/\._\w/)) {
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
_.each(area.items, function (item) {
|
|
331
|
-
const manager = self.apos.area.getWidgetManager(item.type);
|
|
332
|
-
if (!manager) {
|
|
333
|
-
self.apos.area.warnMissingWidgetType(item.type);
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
if (manager.addSearchTexts) {
|
|
337
|
-
manager.addSearchTexts(item, texts);
|
|
338
|
-
}
|
|
339
|
-
});
|
|
340
|
-
});
|
|
341
|
-
|
|
330
|
+
const manager = self.apos.doc.getManager(doc.type);
|
|
331
|
+
if (manager) {
|
|
332
|
+
const schema = manager.schema;
|
|
333
|
+
self.apos.schema.indexFields(schema, doc, texts);
|
|
334
|
+
}
|
|
342
335
|
return texts;
|
|
343
336
|
},
|
|
344
337
|
|
|
@@ -358,6 +351,13 @@ module.exports = {
|
|
|
358
351
|
|
|
359
352
|
docUnversionedFields(req, doc, fields) {
|
|
360
353
|
fields.push('titleSortified', 'highSearchText', 'highSearchWords', 'lowSearchText', 'searchSummary');
|
|
354
|
+
},
|
|
355
|
+
|
|
356
|
+
async indexTask() {
|
|
357
|
+
const req = self.apos.task.getReq();
|
|
358
|
+
return self.apos.migration.eachDoc({}, doc => {
|
|
359
|
+
return self.indexTaskOne(req, doc);
|
|
360
|
+
});
|
|
361
361
|
}
|
|
362
362
|
};
|
|
363
363
|
},
|
|
@@ -366,12 +366,11 @@ module.exports = {
|
|
|
366
366
|
index: {
|
|
367
367
|
usage: stripIndent`
|
|
368
368
|
Rebuild the search index. Normally this happens automatically.
|
|
369
|
-
This should only be needed if you have changed the"searchable" property
|
|
369
|
+
This should only be needed if you have changed the "searchable" property
|
|
370
370
|
for various fields or types.
|
|
371
371
|
`,
|
|
372
|
-
task(argv) {
|
|
373
|
-
|
|
374
|
-
return self.apos.migration.eachDoc({}, _.partial(self.indexTaskOne, req));
|
|
372
|
+
async task(argv) {
|
|
373
|
+
await self.indexTask();
|
|
375
374
|
}
|
|
376
375
|
}
|
|
377
376
|
};
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
|
|
43
43
|
<script>
|
|
44
44
|
import draggable from 'vuedraggable';
|
|
45
|
+
import cuid from 'cuid';
|
|
45
46
|
|
|
46
47
|
export default {
|
|
47
48
|
name: 'AposSlatList',
|
|
@@ -89,7 +90,7 @@ export default {
|
|
|
89
90
|
},
|
|
90
91
|
computed: {
|
|
91
92
|
listId() {
|
|
92
|
-
return `sortableList-${(
|
|
93
|
+
return `sortableList-${cuid()}`;
|
|
93
94
|
},
|
|
94
95
|
dragOptions() {
|
|
95
96
|
return {
|
package/package.json
CHANGED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
const t = require('../test-lib/test.js');
|
|
2
|
+
const assert = require('assert');
|
|
3
|
+
const { klona } = require('klona');
|
|
4
|
+
|
|
5
|
+
describe('Concurrent Array Joins', function() {
|
|
6
|
+
|
|
7
|
+
after(async function() {
|
|
8
|
+
return t.destroy(apos);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
this.timeout(t.timeout);
|
|
12
|
+
|
|
13
|
+
let apos;
|
|
14
|
+
|
|
15
|
+
// EXISTENCE
|
|
16
|
+
|
|
17
|
+
it('should be a property of the apos object', async function() {
|
|
18
|
+
apos = await t.create({
|
|
19
|
+
root: module,
|
|
20
|
+
modules: {
|
|
21
|
+
'test-person': {
|
|
22
|
+
extend: '@apostrophecms/piece-type',
|
|
23
|
+
options: {
|
|
24
|
+
alias: 'person'
|
|
25
|
+
},
|
|
26
|
+
fields: {
|
|
27
|
+
add: {
|
|
28
|
+
hobbies: {
|
|
29
|
+
type: 'array',
|
|
30
|
+
fields: {
|
|
31
|
+
add: {
|
|
32
|
+
name: {
|
|
33
|
+
type: 'string'
|
|
34
|
+
},
|
|
35
|
+
_friends: {
|
|
36
|
+
type: 'relationship',
|
|
37
|
+
withType: 'test-person'
|
|
38
|
+
// builders: {
|
|
39
|
+
// project: {
|
|
40
|
+
// title: 1
|
|
41
|
+
// }
|
|
42
|
+
// }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should be able to retrieve hobbies in parallel with all relationships', async function() {
|
|
55
|
+
const req = apos.task.getReq();
|
|
56
|
+
const hobbyists = [];
|
|
57
|
+
for (let i = 0; (i < 10); i++) {
|
|
58
|
+
hobbyists.push(await apos.person.insert(req, {
|
|
59
|
+
title: `Hobbyist ${i}`,
|
|
60
|
+
visibility: 'public'
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
for (let i = 0; (i < 10); i++) {
|
|
64
|
+
await apos.person.update(req, {
|
|
65
|
+
...hobbyists[i],
|
|
66
|
+
hobbies: [
|
|
67
|
+
{
|
|
68
|
+
name: `Hobby ${i}`,
|
|
69
|
+
_friends: [
|
|
70
|
+
// Deep clone to avoid infinite recursion during the save operation
|
|
71
|
+
// as 4 points to 5 and vice versa
|
|
72
|
+
klona(hobbyists[9 - i])
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
const promises = [];
|
|
79
|
+
for (let i = 0; (i < 100); i++) {
|
|
80
|
+
const req = apos.task.getReq();
|
|
81
|
+
promises.push(apos.person.find(req).toArray());
|
|
82
|
+
}
|
|
83
|
+
const results = await Promise.all(promises);
|
|
84
|
+
assert.strictEqual(results.length, 100);
|
|
85
|
+
for (const result of results) {
|
|
86
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare
|
|
87
|
+
result.sort((a, b) => a.title.localeCompare(b.title));
|
|
88
|
+
assert.strictEqual(result.length, 10);
|
|
89
|
+
for (let i = 0; (i < 10); i++) {
|
|
90
|
+
const person = result[i];
|
|
91
|
+
assert.strictEqual(person.title, `Hobbyist ${i}`);
|
|
92
|
+
console.log(person);
|
|
93
|
+
assert.strictEqual(person.hobbies.length, 1);
|
|
94
|
+
assert.strictEqual(person.hobbies[0].name, `Hobby ${i}`);
|
|
95
|
+
assert(person.hobbies[0]._friends);
|
|
96
|
+
assert(person.hobbies[0]._friends[0]);
|
|
97
|
+
assert.strictEqual(person.hobbies[0]._friends[0].title, `Hobbyist ${9 - i}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
});
|