apostrophe 3.32.0 → 3.33.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 +13 -0
- package/CONTRIBUTING.md +32 -9
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaEditor.vue +5 -2
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +36 -15
- package/modules/@apostrophecms/asset/lib/globalIcons.js +1 -0
- package/modules/@apostrophecms/i18n/i18n/en.json +3 -0
- package/modules/@apostrophecms/piece-type/index.js +38 -0
- package/modules/@apostrophecms/schema/index.js +4 -1
- 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/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,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.33.0 (2022-11-28)
|
|
4
|
+
|
|
5
|
+
### Adds
|
|
6
|
+
|
|
7
|
+
* 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.
|
|
8
|
+
* Batch feature for publishing pieces.
|
|
9
|
+
|
|
10
|
+
### Fixes
|
|
11
|
+
|
|
12
|
+
* Prior to this release, widget templates that contained areas pulled in from related documents would break the ability to add another widget beneath.
|
|
13
|
+
* Validation of object fields now works properly on the browser side, in addition to server-side validation, resolving UX issues.
|
|
14
|
+
* 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.
|
|
15
|
+
|
|
3
16
|
## 3.32.0 (2022-11-09)
|
|
4
17
|
|
|
5
18
|
### 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;
|
|
@@ -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
|
|
@@ -283,11 +289,13 @@ export default {
|
|
|
283
289
|
return false;
|
|
284
290
|
}
|
|
285
291
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
292
|
+
return !(this.hovered || this.nonForeignHovered);
|
|
293
|
+
},
|
|
294
|
+
hovered() {
|
|
295
|
+
return this.widgetHovered === this.widget._id;
|
|
296
|
+
},
|
|
297
|
+
nonForeignHovered() {
|
|
298
|
+
return this.nonForeignWidgetHovered === this.widget._id;
|
|
291
299
|
},
|
|
292
300
|
// Sets up all the interaction classes based on the current
|
|
293
301
|
// state. If our widget is suppressed, return a blank UI state and reset
|
|
@@ -297,11 +305,11 @@ export default {
|
|
|
297
305
|
controls: this.state.controls.show ? this.classes.show : null,
|
|
298
306
|
labels: this.state.labels.show ? this.classes.show : null,
|
|
299
307
|
container: this.state.container.focus ? this.classes.focus
|
|
300
|
-
: (this.state.container.highlight ? this.classes.highlight : null),
|
|
308
|
+
: ((this.state.container.highlight || this.nonForeignHovered) ? this.classes.highlight : null),
|
|
301
309
|
addTop: this.state.add.top.focus ? this.classes.focus
|
|
302
|
-
: (this.state.add.top.show ? this.classes.show : null),
|
|
310
|
+
: ((this.state.add.top.show || this.nonForeignHovered) ? this.classes.show : null),
|
|
303
311
|
addBottom: this.state.add.bottom.focus ? this.classes.focus
|
|
304
|
-
: (this.state.add.bottom.show ? this.classes.show : null)
|
|
312
|
+
: ((this.state.add.bottom.show || this.nonForeignHovered) ? this.classes.show : null)
|
|
305
313
|
};
|
|
306
314
|
|
|
307
315
|
if (this.isSuppressed) {
|
|
@@ -356,9 +364,7 @@ export default {
|
|
|
356
364
|
|
|
357
365
|
this.breadcrumbs.$lastEl = this.$el;
|
|
358
366
|
|
|
359
|
-
|
|
360
|
-
this.getBreadcrumbs();
|
|
361
|
-
}
|
|
367
|
+
this.getBreadcrumbs();
|
|
362
368
|
|
|
363
369
|
if (this.widgetFocused) {
|
|
364
370
|
// If another widget was in focus (because the user clicked the "add"
|
|
@@ -395,11 +401,18 @@ export default {
|
|
|
395
401
|
if (this.focused) {
|
|
396
402
|
return;
|
|
397
403
|
}
|
|
398
|
-
this.
|
|
399
|
-
|
|
404
|
+
if (!this.foreign) {
|
|
405
|
+
this.state.add.top.show = true;
|
|
406
|
+
this.state.add.bottom.show = true;
|
|
407
|
+
}
|
|
400
408
|
this.state.container.highlight = true;
|
|
401
409
|
this.state.labels.show = true;
|
|
402
|
-
|
|
410
|
+
const closest = this.foreign && this.$el.closest('[data-apos-widget-foreign="0"]');
|
|
411
|
+
const closestId = closest && closest.getAttribute('data-apos-widget-id');
|
|
412
|
+
apos.bus.$emit('widget-hover', {
|
|
413
|
+
_id: this.widget._id,
|
|
414
|
+
nonForeignId: this.foreign ? closestId : null
|
|
415
|
+
});
|
|
403
416
|
},
|
|
404
417
|
|
|
405
418
|
mouseleave() {
|
|
@@ -412,6 +425,12 @@ export default {
|
|
|
412
425
|
this.state.add.top.show = false;
|
|
413
426
|
this.state.add.bottom.show = false;
|
|
414
427
|
}
|
|
428
|
+
if (this.hovered) {
|
|
429
|
+
apos.bus.$emit('widget-hover', {
|
|
430
|
+
_id: null,
|
|
431
|
+
nonForeignId: null
|
|
432
|
+
});
|
|
433
|
+
}
|
|
415
434
|
},
|
|
416
435
|
|
|
417
436
|
focus(e) {
|
|
@@ -432,7 +451,7 @@ export default {
|
|
|
432
451
|
this.focused = false;
|
|
433
452
|
this.resetState();
|
|
434
453
|
this.highlightable = false;
|
|
435
|
-
document.removeEventListener('click', this.
|
|
454
|
+
document.removeEventListener('click', this.unfocus);
|
|
436
455
|
apos.bus.$emit('widget-focus', null);
|
|
437
456
|
}
|
|
438
457
|
},
|
|
@@ -477,9 +496,11 @@ export default {
|
|
|
477
496
|
widgetComponent(type) {
|
|
478
497
|
return this.moduleOptions.components.widgets[type];
|
|
479
498
|
},
|
|
499
|
+
|
|
480
500
|
widgetEditorComponent(type) {
|
|
481
501
|
return this.moduleOptions.components.widgetEditors[type];
|
|
482
502
|
}
|
|
503
|
+
|
|
483
504
|
}
|
|
484
505
|
};
|
|
485
506
|
</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',
|
|
@@ -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)",
|
|
@@ -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');
|
|
@@ -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;
|
|
@@ -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 & {
|
|
@@ -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
|
+
});
|