apostrophe 3.65.0 → 3.67.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 +24 -7
- package/modules/@apostrophecms/i18n/i18n/en.json +2 -0
- package/modules/@apostrophecms/image/index.js +185 -20
- package/modules/@apostrophecms/page/index.js +9 -6
- package/modules/@apostrophecms/schema/index.js +33 -8
- package/modules/@apostrophecms/schema/lib/addFieldTypes.js +9 -4
- package/modules/@apostrophecms/schema/ui/apos/components/AposSchema.vue +7 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposSchema.js +3 -0
- package/modules/@apostrophecms/ui/ui/apos/scss/global/_theme.scss +1 -0
- package/package.json +1 -1
- package/test/images.js +140 -2
- package/test/pages.js +227 -0
- package/test/schemas.js +322 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,17 +1,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 3.
|
|
3
|
+
## 3.67.0 (2024-06-12)
|
|
4
4
|
|
|
5
|
-
###
|
|
5
|
+
### Changes
|
|
6
|
+
|
|
7
|
+
* When moving a page, recognize when the slug of a new child
|
|
8
|
+
already contains the new parent's slug and not double it.
|
|
9
|
+
For example, given we have two pages as children of the home page, page A and page B.
|
|
10
|
+
Page A and page B are siblings.
|
|
11
|
+
Page A has the slug `/peer` and page B has the slug `/peer/page`.
|
|
12
|
+
Now we want page B to be the child of page A.
|
|
13
|
+
We will now end up with page B slug as `/peer/page` and not `/peer/peer/page` as before.
|
|
14
|
+
|
|
15
|
+
### Fixes
|
|
16
|
+
|
|
17
|
+
* Updating schema fields as read-only no longer reset the value when updating the document.
|
|
18
|
+
|
|
19
|
+
## 3.66.0 (2024-05-15)
|
|
6
20
|
|
|
7
|
-
|
|
21
|
+
### Fixes
|
|
22
|
+
|
|
23
|
+
* Autocrop image attachments for referenced documents when replacing an image in the Media Manager.
|
|
24
|
+
* Backports some internal A4 UI logic for metadata to make the new `document-versions` comparison feature compatible with A3.
|
|
25
|
+
|
|
26
|
+
## 3.65.0 (2024-05-06)
|
|
27
|
+
|
|
28
|
+
* Adds a `publicBundle` option to `@apostrophecms/asset`. When set to `false`, the `ui/src` public asset bundle is not built at all in most cases
|
|
8
29
|
except as part of the admin UI bundle which depends on it. For use with external front ends such as [apostrophe-astro](https://github.com/apostrophecms/apostrophe-astro).
|
|
9
30
|
Thanks to Michelin for contributing this feature.
|
|
10
31
|
|
|
11
32
|
## 3.64.0 (2024-04-18)
|
|
12
33
|
|
|
13
|
-
### Adds
|
|
14
|
-
|
|
15
34
|
### Fixes
|
|
16
35
|
|
|
17
36
|
* Add the missing `metaType` property to newly inserted widgets.
|
|
@@ -42,8 +61,6 @@ that these keys would be present.
|
|
|
42
61
|
* `field.help` and `field.htmlHelp` are now correctly translated when displayed in a tooltip.
|
|
43
62
|
This was also an expectation for the multisite module.
|
|
44
63
|
|
|
45
|
-
## UNRELEASED
|
|
46
|
-
|
|
47
64
|
### Adds
|
|
48
65
|
|
|
49
66
|
* Add side by side comparison support in AposSchema component.
|
|
@@ -194,6 +194,8 @@
|
|
|
194
194
|
"hideInNavigation": "Hide in Navigation",
|
|
195
195
|
"home": "Home",
|
|
196
196
|
"image": "Image",
|
|
197
|
+
"imageOnReplaceAutocropMessage": "The replaced image has been autocropped for \"{{ title }}\".",
|
|
198
|
+
"imageOnReplaceAutocropError": "An error occurred while autocropping the replaced image for {{ title }}",
|
|
197
199
|
"imageDescription": "Add an inline image",
|
|
198
200
|
"imageFile": "Image File",
|
|
199
201
|
"imagePlaceholder": "Image placeholder",
|
|
@@ -122,6 +122,19 @@ module.exports = {
|
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
124
|
},
|
|
125
|
+
handlers(self) {
|
|
126
|
+
return {
|
|
127
|
+
beforeUpdate: {
|
|
128
|
+
// Ensure the crop fields are updated on publishing an image
|
|
129
|
+
async autoCropRelations(req, piece, options) {
|
|
130
|
+
if (piece.aposMode !== 'published') {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
await self.updateImageCropRelationships(req, piece);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
},
|
|
125
138
|
commands(self) {
|
|
126
139
|
return {
|
|
127
140
|
remove: [
|
|
@@ -254,26 +267,7 @@ module.exports = {
|
|
|
254
267
|
return withinOnePercent(testRatio, configuredRatio);
|
|
255
268
|
}
|
|
256
269
|
async function autocrop(image, widgetOptions) {
|
|
257
|
-
const
|
|
258
|
-
const configuredRatio = widgetOptions.aspectRatio[0] / widgetOptions.aspectRatio[1];
|
|
259
|
-
let crop;
|
|
260
|
-
if (configuredRatio >= nativeRatio) {
|
|
261
|
-
const height = image.attachment.width / configuredRatio;
|
|
262
|
-
crop = {
|
|
263
|
-
top: Math.floor((image.attachment.height - height) / 2),
|
|
264
|
-
left: 0,
|
|
265
|
-
width: image.attachment.width,
|
|
266
|
-
height: Math.floor(height)
|
|
267
|
-
};
|
|
268
|
-
} else {
|
|
269
|
-
const width = image.attachment.height * configuredRatio;
|
|
270
|
-
crop = {
|
|
271
|
-
top: 0,
|
|
272
|
-
left: Math.floor((image.attachment.width - width) / 2),
|
|
273
|
-
width: Math.floor(width),
|
|
274
|
-
height: image.attachment.height
|
|
275
|
-
};
|
|
276
|
-
}
|
|
270
|
+
const crop = self.calculateAutocrop(image, widgetOptions.aspectRatio);
|
|
277
271
|
await self.apos.attachment.crop(req, image.attachment._id, crop);
|
|
278
272
|
image._fields = crop;
|
|
279
273
|
// For ease of testing send back the cropped image URLs now
|
|
@@ -457,7 +451,169 @@ module.exports = {
|
|
|
457
451
|
name: 'dummy',
|
|
458
452
|
width: 0
|
|
459
453
|
}).name;
|
|
454
|
+
},
|
|
455
|
+
// Given an image piece and a aspect ratio array, calculate the crop
|
|
456
|
+
// that would be applied to the image when autocropping it to the
|
|
457
|
+
// given aspect ratio.
|
|
458
|
+
calculateAutocrop(image, aspecRatio) {
|
|
459
|
+
let crop;
|
|
460
|
+
const configuredRatio = aspecRatio[0] / aspecRatio[1];
|
|
461
|
+
const nativeRatio = image.attachment.width / image.attachment.height;
|
|
462
|
+
|
|
463
|
+
if (configuredRatio >= nativeRatio) {
|
|
464
|
+
const height = image.attachment.width / configuredRatio;
|
|
465
|
+
crop = {
|
|
466
|
+
top: Math.floor((image.attachment.height - height) / 2),
|
|
467
|
+
left: 0,
|
|
468
|
+
width: image.attachment.width,
|
|
469
|
+
height: Math.floor(height)
|
|
470
|
+
};
|
|
471
|
+
} else {
|
|
472
|
+
const width = image.attachment.height * configuredRatio;
|
|
473
|
+
crop = {
|
|
474
|
+
top: 0,
|
|
475
|
+
left: Math.floor((image.attachment.width - width) / 2),
|
|
476
|
+
width: Math.floor(width),
|
|
477
|
+
height: image.attachment.height
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return crop;
|
|
482
|
+
},
|
|
483
|
+
async updateImageCropRelationships(req, piece) {
|
|
484
|
+
if (!piece.relatedReverseIds?.length) {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
if (
|
|
488
|
+
!piece._prevAttachmentId ||
|
|
489
|
+
!piece.attachment ||
|
|
490
|
+
piece._prevAttachmentId === piece.attachment._id
|
|
491
|
+
) {
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
const croppedIndex = {};
|
|
495
|
+
for (const docId of piece.relatedReverseIds) {
|
|
496
|
+
await self.updateImageCropsForRelationship(req, docId, piece, croppedIndex);
|
|
497
|
+
}
|
|
498
|
+
},
|
|
499
|
+
// This handler operates on all documents of a given aposDocId. The `piece`
|
|
500
|
+
// argument is the image piece that has been updated.
|
|
501
|
+
// - Auto re-crop the image, using the same width/height ratio if the
|
|
502
|
+
// image has been cropped.
|
|
503
|
+
// - Remove any existing focal point data.
|
|
504
|
+
//
|
|
505
|
+
// `croppedIndex` is used to avoid re-cropping the same image when updating multiple
|
|
506
|
+
// documents. It's internally mutated by the handler.
|
|
507
|
+
async updateImageCropsForRelationship(req, aposDocId, piece, croppedIndex = {}) {
|
|
508
|
+
const dbDocs = await self.apos.doc.db.find({
|
|
509
|
+
aposDocId
|
|
510
|
+
}).toArray();
|
|
511
|
+
const changeSets = dbDocs.flatMap(doc => getDocRelations(doc, piece));
|
|
512
|
+
for (const changeSet of changeSets) {
|
|
513
|
+
try {
|
|
514
|
+
const cropFields = await autocrop(changeSet.image, changeSet.cropFields, croppedIndex);
|
|
515
|
+
const $set = {
|
|
516
|
+
[changeSet.docDotPath]: cropFields
|
|
517
|
+
};
|
|
518
|
+
self.logDebug(req, 'replace-autocrop', {
|
|
519
|
+
docId: changeSet.docId,
|
|
520
|
+
docTitle: changeSet.doc.title,
|
|
521
|
+
imageId: piece._id,
|
|
522
|
+
imageTitle: piece.title,
|
|
523
|
+
$set
|
|
524
|
+
});
|
|
525
|
+
await self.apos.doc.db.updateOne({
|
|
526
|
+
_id: changeSet.docId
|
|
527
|
+
}, {
|
|
528
|
+
$set
|
|
529
|
+
});
|
|
530
|
+
} catch (e) {
|
|
531
|
+
self.apos.util.error(e);
|
|
532
|
+
await self.apos.notify(
|
|
533
|
+
req,
|
|
534
|
+
req.t(
|
|
535
|
+
'apostrophe:imageOnReplaceAutocropError',
|
|
536
|
+
{
|
|
537
|
+
title: changeSet.doc.title
|
|
538
|
+
}
|
|
539
|
+
),
|
|
540
|
+
{
|
|
541
|
+
type: 'danger',
|
|
542
|
+
dismiss: true
|
|
543
|
+
}
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (changeSets.length) {
|
|
549
|
+
return self.apos.notify(
|
|
550
|
+
req,
|
|
551
|
+
req.t(
|
|
552
|
+
'apostrophe:imageOnReplaceAutocropMessage',
|
|
553
|
+
{
|
|
554
|
+
title: changeSets[0].doc.title
|
|
555
|
+
}
|
|
556
|
+
),
|
|
557
|
+
{
|
|
558
|
+
type: 'success',
|
|
559
|
+
dismiss: true
|
|
560
|
+
}
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function getDocRelations(doc, imagePiece) {
|
|
565
|
+
const results = [];
|
|
566
|
+
self.apos.doc.walk(doc, function (o, key, value, dotPath, ancestors) {
|
|
567
|
+
if (!value || typeof value !== 'object' || !Array.isArray(value.imageIds)) {
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
if (!value.imageIds.includes(imagePiece.aposDocId)) {
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
if (!value.imageFields?.[imagePiece.aposDocId]) {
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
const cropFields = value.imageFields[imagePiece.aposDocId];
|
|
577
|
+
// We check for crop OR focal point data (because
|
|
578
|
+
// focal point has to be reset when the image is replaced).
|
|
579
|
+
if (!cropFields.width && typeof cropFields.x !== 'number') {
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
results.push({
|
|
583
|
+
docId: doc._id,
|
|
584
|
+
docDotPath: `${dotPath}.imageFields.${imagePiece.aposDocId}`,
|
|
585
|
+
doc,
|
|
586
|
+
cropFields,
|
|
587
|
+
image: imagePiece,
|
|
588
|
+
value
|
|
589
|
+
});
|
|
590
|
+
});
|
|
591
|
+
return results;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
async function autocrop(image, oldFields, croppedIndex) {
|
|
595
|
+
let crop = { ...oldFields };
|
|
596
|
+
if (crop.width) {
|
|
597
|
+
crop = self.calculateAutocrop(image, [ crop.width, crop.height ]);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const hash = cropHash(image, crop);
|
|
601
|
+
if (crop.width && !croppedIndex[hash]) {
|
|
602
|
+
await self.apos.attachment.crop(req, image.attachment._id, crop);
|
|
603
|
+
croppedIndex[hash] = true;
|
|
604
|
+
}
|
|
605
|
+
return {
|
|
606
|
+
...crop,
|
|
607
|
+
x: null,
|
|
608
|
+
y: null
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function cropHash(image, crop) {
|
|
613
|
+
return `${image.attachment._id}-${crop.top}-${crop.left}-${crop.width}-${crop.height}`;
|
|
614
|
+
}
|
|
460
615
|
}
|
|
616
|
+
|
|
461
617
|
};
|
|
462
618
|
},
|
|
463
619
|
extendMethods(self) {
|
|
@@ -515,6 +671,15 @@ module.exports = {
|
|
|
515
671
|
}
|
|
516
672
|
return [ self.apos.launder.integer(a[0]), self.apos.launder.integer(a[1]) ];
|
|
517
673
|
}
|
|
674
|
+
},
|
|
675
|
+
prevAttachment: {
|
|
676
|
+
after(results) {
|
|
677
|
+
for (const result of results) {
|
|
678
|
+
if (result.attachment) {
|
|
679
|
+
result._prevAttachmentId = result.attachment._id;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
518
683
|
}
|
|
519
684
|
}
|
|
520
685
|
};
|
|
@@ -1275,11 +1275,14 @@ database.`);
|
|
|
1275
1275
|
if (parent._id !== oldParent._id) {
|
|
1276
1276
|
const matchOldParentSlugPrefix = new RegExp('^' + self.apos.util.regExpQuote(self.apos.util.addSlashIfNeeded(oldParent.slug)));
|
|
1277
1277
|
if (moved.slug.match(matchOldParentSlugPrefix)) {
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1278
|
+
const movedSlugCandidate = moved.slug
|
|
1279
|
+
.split('/')
|
|
1280
|
+
.slice(0, -1)
|
|
1281
|
+
.join('/');
|
|
1282
|
+
|
|
1283
|
+
moved.slug = parent.slug.endsWith(movedSlugCandidate)
|
|
1284
|
+
? parent.slug.replace(movedSlugCandidate, '').concat(moved.slug)
|
|
1285
|
+
: moved.slug.replace(matchOldParentSlugPrefix, self.apos.util.addSlashIfNeeded(parent.slug));
|
|
1283
1286
|
changed.push({
|
|
1284
1287
|
_id: moved._id,
|
|
1285
1288
|
slug: moved.slug
|
|
@@ -2300,7 +2303,7 @@ database.`);
|
|
|
2300
2303
|
// Apostrophe queries used to fetch Apostrophe pages
|
|
2301
2304
|
// consult this method.
|
|
2302
2305
|
getBaseUrl(req) {
|
|
2303
|
-
const hostname = self.apos.i18n.locales[req.locale]
|
|
2306
|
+
const hostname = self.apos.i18n.locales[req.locale]?.hostname;
|
|
2304
2307
|
|
|
2305
2308
|
return hostname
|
|
2306
2309
|
? `${req.protocol}://${hostname}`
|
|
@@ -441,6 +441,13 @@ module.exports = {
|
|
|
441
441
|
});
|
|
442
442
|
},
|
|
443
443
|
|
|
444
|
+
// Wrapper around isEqual method to get modified fields between two documents
|
|
445
|
+
// instead of just getting a boolean, it will return an array of the modified fields
|
|
446
|
+
|
|
447
|
+
getChanges(req, schema, one, two) {
|
|
448
|
+
return self.isEqual(req, schema, one, two, { getChanges: true });
|
|
449
|
+
},
|
|
450
|
+
|
|
444
451
|
// Compare two objects and return true only if their schema fields are equal.
|
|
445
452
|
//
|
|
446
453
|
// Note that for relationship fields this comparison is based on the idsStorage
|
|
@@ -451,22 +458,40 @@ module.exports = {
|
|
|
451
458
|
// This method is invoked by the doc module to compare draft and published
|
|
452
459
|
// documents and set the modified property of the draft, just before updating the
|
|
453
460
|
// published version.
|
|
461
|
+
//
|
|
462
|
+
// When passing the option `getChange: true` it'll return an array of changed fields
|
|
463
|
+
// in this case the method won't short circuit by directly returning false
|
|
464
|
+
// when finding a changed field
|
|
454
465
|
|
|
455
|
-
isEqual(req, schema, one, two) {
|
|
466
|
+
isEqual(req, schema, one, two, options = {}) {
|
|
467
|
+
const changedFields = [];
|
|
456
468
|
for (const field of schema) {
|
|
457
469
|
const fieldType = self.fieldTypes[field.type];
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
!((one[field.name] == null) && (two[field.name] == null))) {
|
|
461
|
-
return false;
|
|
462
|
-
}
|
|
463
|
-
} else {
|
|
470
|
+
|
|
471
|
+
if (fieldType.isEqual) {
|
|
464
472
|
if (!fieldType.isEqual(req, field, one, two)) {
|
|
473
|
+
if (options.getChanges) {
|
|
474
|
+
changedFields.push(field.name);
|
|
475
|
+
} else {
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (
|
|
483
|
+
!_.isEqual(one[field.name], two[field.name]) &&
|
|
484
|
+
!((one[field.name] == null) && (two[field.name] == null))
|
|
485
|
+
) {
|
|
486
|
+
if (options.getChanges) {
|
|
487
|
+
changedFields.push(field.name);
|
|
488
|
+
} else {
|
|
465
489
|
return false;
|
|
466
490
|
}
|
|
467
491
|
}
|
|
468
492
|
}
|
|
469
|
-
|
|
493
|
+
|
|
494
|
+
return options.getChanges ? changedFields : true;
|
|
470
495
|
},
|
|
471
496
|
|
|
472
497
|
// Index the object's fields for participation in Apostrophe search unless
|
|
@@ -786,8 +786,12 @@ module.exports = (self) => {
|
|
|
786
786
|
}
|
|
787
787
|
const errors = [];
|
|
788
788
|
for (const datum of data) {
|
|
789
|
-
const
|
|
790
|
-
|
|
789
|
+
const _id = self.apos.launder.id(datum._id) || self.apos.util.generateId();
|
|
790
|
+
const [ found ] = destination[field.name]?.filter?.(item => item._id === _id) || [];
|
|
791
|
+
const result = {
|
|
792
|
+
...(found || {}),
|
|
793
|
+
_id
|
|
794
|
+
};
|
|
791
795
|
result.metaType = 'arrayItem';
|
|
792
796
|
result.scopedArrayName = field.scopedArrayName;
|
|
793
797
|
try {
|
|
@@ -859,7 +863,7 @@ module.exports = (self) => {
|
|
|
859
863
|
if (one[field.name].length !== two[field.name].length) {
|
|
860
864
|
return false;
|
|
861
865
|
}
|
|
862
|
-
for (let i = 0; (i < one.length); i++) {
|
|
866
|
+
for (let i = 0; (i < one[field.name].length); i++) {
|
|
863
867
|
if (!self.isEqual(req, field.schema, one[field.name][i], two[field.name][i])) {
|
|
864
868
|
return false;
|
|
865
869
|
}
|
|
@@ -876,6 +880,7 @@ module.exports = (self) => {
|
|
|
876
880
|
const schema = field.schema;
|
|
877
881
|
const errors = [];
|
|
878
882
|
const result = {
|
|
883
|
+
...(destination[field.name] || {}),
|
|
879
884
|
_id: self.apos.launder.id(data && data._id) || self.apos.util.generateId()
|
|
880
885
|
};
|
|
881
886
|
if (data == null || typeof data !== 'object' || Array.isArray(data)) {
|
|
@@ -1027,7 +1032,7 @@ module.exports = (self) => {
|
|
|
1027
1032
|
const result = results.find(doc => (doc._id === item._id));
|
|
1028
1033
|
if (result) {
|
|
1029
1034
|
if (field.schema) {
|
|
1030
|
-
result._fields = {};
|
|
1035
|
+
result._fields = { ...(destination[field.name]?.find?.(doc => doc._id === item._id)?._fields || {}) };
|
|
1031
1036
|
if (item && ((typeof item._fields === 'object'))) {
|
|
1032
1037
|
await self.convert(req, field.schema, item._fields || {}, result._fields);
|
|
1033
1038
|
}
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
<component
|
|
41
41
|
v-show="displayComponent(field)"
|
|
42
42
|
v-model="fieldState[field.name]"
|
|
43
|
+
:class="{ 'apos-field__wrapper--highlight': highlight(field.name) }"
|
|
43
44
|
:is="fieldComponentMap[field.type]"
|
|
44
45
|
:following-values="followingValues[field.name]"
|
|
45
46
|
:condition-met="conditionalFields?.if[field.name]"
|
|
@@ -59,6 +60,7 @@
|
|
|
59
60
|
v-if="hasCompareMeta"
|
|
60
61
|
v-show="displayComponent(field)"
|
|
61
62
|
v-model="compareMetaState[field.name]"
|
|
63
|
+
:class="{ 'apos-field__wrapper--highlight': highlight(field.name) }"
|
|
62
64
|
:is="fieldComponentMap[field.type]"
|
|
63
65
|
:following-values="followingValues[field.name]"
|
|
64
66
|
:condition-met="conditionalFields?.if[field.name]"
|
|
@@ -139,4 +141,9 @@ export default {
|
|
|
139
141
|
}
|
|
140
142
|
}
|
|
141
143
|
}
|
|
144
|
+
|
|
145
|
+
:deep(.apos-field__wrapper--highlight > .apos-field) {
|
|
146
|
+
padding: 10px;
|
|
147
|
+
background: var(--a-highlight);
|
|
148
|
+
}
|
|
142
149
|
</style>
|
package/package.json
CHANGED
package/test/images.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
const t = require('../test-lib/test.js');
|
|
2
|
-
const assert = require('assert');
|
|
2
|
+
const assert = require('assert/strict');
|
|
3
3
|
const fs = require('fs');
|
|
4
|
+
const fsp = require('fs/promises');
|
|
4
5
|
const path = require('path');
|
|
5
6
|
const FormData = require('form-data');
|
|
6
7
|
|
|
8
|
+
const publicFolderPath = path.join(process.cwd(), 'test/public');
|
|
9
|
+
|
|
7
10
|
describe('Images', function() {
|
|
8
11
|
|
|
9
12
|
let apos;
|
|
@@ -73,7 +76,8 @@ describe('Images', function() {
|
|
|
73
76
|
// Test pieces.list()
|
|
74
77
|
it('should clean up any existing images for testing', async function() {
|
|
75
78
|
try {
|
|
76
|
-
const response = await apos.doc.db.deleteMany(
|
|
79
|
+
const response = await apos.doc.db.deleteMany(
|
|
80
|
+
{ type: '@apostrophecms/image' }
|
|
77
81
|
);
|
|
78
82
|
assert(response.result.ok === 1);
|
|
79
83
|
} catch (e) {
|
|
@@ -263,6 +267,140 @@ describe('Images', function() {
|
|
|
263
267
|
assert.strictEqual(fields.height, 225);
|
|
264
268
|
});
|
|
265
269
|
|
|
270
|
+
it('should update crop fields when replacing an image attachment', async function () {
|
|
271
|
+
await t.destroy(apos);
|
|
272
|
+
await fsp.rm(path.join(publicFolderPath, 'uploads'), {
|
|
273
|
+
recursive: true,
|
|
274
|
+
force: true
|
|
275
|
+
});
|
|
276
|
+
apos = await t.create({
|
|
277
|
+
root: module,
|
|
278
|
+
modules: {
|
|
279
|
+
'test-piece': {
|
|
280
|
+
extend: '@apostrophecms/piece-type',
|
|
281
|
+
fields: {
|
|
282
|
+
add: {
|
|
283
|
+
main: {
|
|
284
|
+
type: 'area',
|
|
285
|
+
options: {
|
|
286
|
+
widgets: {
|
|
287
|
+
'@apostrophecms/image': {
|
|
288
|
+
aspectRatio: [ 3, 2 ]
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
await insertUser({
|
|
299
|
+
title: 'admin',
|
|
300
|
+
username: 'admin',
|
|
301
|
+
password: 'admin',
|
|
302
|
+
email: 'ad@min.com',
|
|
303
|
+
role: 'admin'
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// Upload an image (landscape), crop it, insert a piece with the cropped image
|
|
307
|
+
jar = await login('admin');
|
|
308
|
+
const formData = new FormData();
|
|
309
|
+
const stream = fs.createReadStream(
|
|
310
|
+
path.join(apos.rootDir, '/public/test-image-landscape.jpg')
|
|
311
|
+
);
|
|
312
|
+
formData.append('file', stream);
|
|
313
|
+
const attachment = await apos.http.post('/api/v1/@apostrophecms/attachment/upload', {
|
|
314
|
+
body: formData,
|
|
315
|
+
jar
|
|
316
|
+
});
|
|
317
|
+
stream.close();
|
|
318
|
+
image = await apos.http.post('/api/v1/@apostrophecms/image', {
|
|
319
|
+
body: {
|
|
320
|
+
title: 'Test Image Landscape',
|
|
321
|
+
attachment
|
|
322
|
+
},
|
|
323
|
+
jar
|
|
324
|
+
});
|
|
325
|
+
assert.equal(image._prevAttachmentId, attachment._id);
|
|
326
|
+
const crop = await apos.http.post('/api/v1/@apostrophecms/image/autocrop', {
|
|
327
|
+
body: {
|
|
328
|
+
relationship: [ image ],
|
|
329
|
+
widgetOptions: {
|
|
330
|
+
aspectRatio: [ 3, 2 ]
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
jar
|
|
334
|
+
});
|
|
335
|
+
let piece = await apos.http.post('/api/v1/test-piece', {
|
|
336
|
+
jar,
|
|
337
|
+
body: {
|
|
338
|
+
title: 'Test Piece',
|
|
339
|
+
slug: 'test-piece',
|
|
340
|
+
type: 'test-piece',
|
|
341
|
+
main: {
|
|
342
|
+
metaType: 'area',
|
|
343
|
+
items: [
|
|
344
|
+
{
|
|
345
|
+
type: '@apostrophecms/image',
|
|
346
|
+
metaType: 'widget',
|
|
347
|
+
imageIds: [ image.aposDocId ],
|
|
348
|
+
imageFields: {
|
|
349
|
+
[image.aposDocId]: crop.relationship[0]._fields
|
|
350
|
+
},
|
|
351
|
+
_image: [ crop.relationship[0] ]
|
|
352
|
+
}
|
|
353
|
+
]
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
let imageFields = piece.main.items[0].imageFields[image.aposDocId];
|
|
359
|
+
assert(imageFields, 'imageFields should be present when creating the piece');
|
|
360
|
+
assert.equal(imageFields.width / imageFields.height, 3 / 2, 'aspect ratio should be 3:2');
|
|
361
|
+
await fsp.access(
|
|
362
|
+
path.join(
|
|
363
|
+
publicFolderPath,
|
|
364
|
+
attachment._urls.original.replace(
|
|
365
|
+
'.jpg',
|
|
366
|
+
`.${imageFields.left}.${imageFields.top}.${imageFields.width}.${imageFields.height}.jpg`
|
|
367
|
+
)
|
|
368
|
+
)
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
// Replace the image with portrait orientation, verify that the aspect ratio is preserved
|
|
372
|
+
const formDataPortrait = new FormData();
|
|
373
|
+
const streamPortrait = fs.createReadStream(path.join(apos.rootDir, '/public/test-image.jpg'));
|
|
374
|
+
formDataPortrait.append('file', streamPortrait);
|
|
375
|
+
const attachmentPortrait = await apos.http.post('/api/v1/@apostrophecms/attachment/upload', {
|
|
376
|
+
body: formDataPortrait,
|
|
377
|
+
jar
|
|
378
|
+
});
|
|
379
|
+
image = await apos.http.put(`/api/v1/@apostrophecms/image/${image._id}`, {
|
|
380
|
+
body: {
|
|
381
|
+
title: 'Test Image Portrait',
|
|
382
|
+
attachment: attachmentPortrait
|
|
383
|
+
},
|
|
384
|
+
jar
|
|
385
|
+
});
|
|
386
|
+
streamPortrait.close();
|
|
387
|
+
piece = await apos.http.get(`/api/v1/test-piece/${piece._id}`, {
|
|
388
|
+
jar
|
|
389
|
+
});
|
|
390
|
+
imageFields = piece.main.items[0].imageFields[image.aposDocId];
|
|
391
|
+
assert(imageFields, 'imageFields should be present after replacing the image attachment');
|
|
392
|
+
assert.equal(imageFields.width / imageFields.height, 3 / 2, 'aspect ratio should be 3:2');
|
|
393
|
+
await fsp.access(
|
|
394
|
+
path.join(
|
|
395
|
+
publicFolderPath,
|
|
396
|
+
attachmentPortrait._urls.original.replace(
|
|
397
|
+
'.jpg',
|
|
398
|
+
`.${imageFields.left}.${imageFields.top}.${imageFields.width}.${imageFields.height}.jpg`
|
|
399
|
+
)
|
|
400
|
+
)
|
|
401
|
+
);
|
|
402
|
+
});
|
|
403
|
+
|
|
266
404
|
async function insertUser(info) {
|
|
267
405
|
const user = apos.user.newInstance();
|
|
268
406
|
assert(user);
|
package/test/pages.js
CHANGED
|
@@ -457,6 +457,233 @@ describe('Pages', function() {
|
|
|
457
457
|
assert.strictEqual(page.path, `${homeId.replace(':en:published', '')}/another-parent/parent/sibling`);
|
|
458
458
|
});
|
|
459
459
|
|
|
460
|
+
describe('move peer pages', function () {
|
|
461
|
+
this.afterEach(async function() {
|
|
462
|
+
await apos.doc.db.deleteMany({
|
|
463
|
+
type: 'test-page'
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('moving /bar under /foo should wind up with /foo/bar', async function() {
|
|
468
|
+
const fooPage = await apos.page.insert(
|
|
469
|
+
apos.task.getReq(),
|
|
470
|
+
'_home',
|
|
471
|
+
'lastChild',
|
|
472
|
+
{
|
|
473
|
+
slug: '/foo',
|
|
474
|
+
visibility: 'public',
|
|
475
|
+
type: 'test-page',
|
|
476
|
+
title: 'Foo Page'
|
|
477
|
+
}
|
|
478
|
+
);
|
|
479
|
+
const barPage = await apos.page.insert(
|
|
480
|
+
apos.task.getReq(),
|
|
481
|
+
'_home',
|
|
482
|
+
'lastChild',
|
|
483
|
+
{
|
|
484
|
+
slug: '/bar',
|
|
485
|
+
visibility: 'public',
|
|
486
|
+
type: 'test-page',
|
|
487
|
+
title: 'Bar Page'
|
|
488
|
+
}
|
|
489
|
+
);
|
|
490
|
+
const childPage = await apos.page.insert(
|
|
491
|
+
apos.task.getReq(),
|
|
492
|
+
barPage._id,
|
|
493
|
+
'lastChild',
|
|
494
|
+
{
|
|
495
|
+
slug: '/bar/child',
|
|
496
|
+
visibility: 'public',
|
|
497
|
+
type: 'test-page',
|
|
498
|
+
title: 'Child Page'
|
|
499
|
+
}
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
await apos.page.move(
|
|
503
|
+
apos.task.getReq(),
|
|
504
|
+
barPage._id,
|
|
505
|
+
fooPage._id,
|
|
506
|
+
'lastChild'
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
const movedPage = await apos.page.find(apos.task.getAnonReq(), { _id: barPage._id }).toObject();
|
|
510
|
+
const movedChildPage = await apos.page.find(apos.task.getAnonReq(), { _id: childPage._id }).toObject();
|
|
511
|
+
|
|
512
|
+
const actual = {
|
|
513
|
+
bar: {
|
|
514
|
+
path: movedPage.path,
|
|
515
|
+
rank: movedPage.rank,
|
|
516
|
+
slug: movedPage.slug
|
|
517
|
+
},
|
|
518
|
+
child: {
|
|
519
|
+
path: movedChildPage.path,
|
|
520
|
+
rank: movedChildPage.rank,
|
|
521
|
+
slug: movedChildPage.slug
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
const expected = {
|
|
525
|
+
bar: {
|
|
526
|
+
path: fooPage.path.concat('/', barPage.aposDocId),
|
|
527
|
+
rank: 0,
|
|
528
|
+
slug: '/foo/bar'
|
|
529
|
+
},
|
|
530
|
+
child: {
|
|
531
|
+
path: movedPage.path.concat('/', childPage.aposDocId),
|
|
532
|
+
rank: 0,
|
|
533
|
+
slug: '/foo/bar/child'
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
assert.deepEqual(actual, expected);
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
it('moving peer /foo/bar under /foo should wind up with /foo/bar', async function() {
|
|
541
|
+
const fooPage = await apos.page.insert(
|
|
542
|
+
apos.task.getReq(),
|
|
543
|
+
'_home',
|
|
544
|
+
'lastChild',
|
|
545
|
+
{
|
|
546
|
+
slug: '/foo',
|
|
547
|
+
visibility: 'public',
|
|
548
|
+
type: 'test-page',
|
|
549
|
+
title: 'Foo Page'
|
|
550
|
+
}
|
|
551
|
+
);
|
|
552
|
+
const barPage = await apos.page.insert(
|
|
553
|
+
apos.task.getReq(),
|
|
554
|
+
'_home',
|
|
555
|
+
'lastChild',
|
|
556
|
+
{
|
|
557
|
+
slug: '/foo/bar',
|
|
558
|
+
visibility: 'public',
|
|
559
|
+
type: 'test-page',
|
|
560
|
+
title: 'Bar Page'
|
|
561
|
+
}
|
|
562
|
+
);
|
|
563
|
+
const childPage = await apos.page.insert(
|
|
564
|
+
apos.task.getReq(),
|
|
565
|
+
barPage._id,
|
|
566
|
+
'lastChild',
|
|
567
|
+
{
|
|
568
|
+
slug: '/foo/bar/child',
|
|
569
|
+
visibility: 'public',
|
|
570
|
+
type: 'test-page',
|
|
571
|
+
title: 'Child Page'
|
|
572
|
+
}
|
|
573
|
+
);
|
|
574
|
+
|
|
575
|
+
await apos.page.move(
|
|
576
|
+
apos.task.getReq(),
|
|
577
|
+
barPage._id,
|
|
578
|
+
fooPage._id,
|
|
579
|
+
'lastChild'
|
|
580
|
+
);
|
|
581
|
+
|
|
582
|
+
const movedPage = await apos.page.find(apos.task.getAnonReq(), { _id: barPage._id }).toObject();
|
|
583
|
+
const movedChildPage = await apos.page.find(apos.task.getAnonReq(), { _id: childPage._id }).toObject();
|
|
584
|
+
|
|
585
|
+
const actual = {
|
|
586
|
+
bar: {
|
|
587
|
+
path: movedPage.path,
|
|
588
|
+
rank: movedPage.rank,
|
|
589
|
+
slug: movedPage.slug
|
|
590
|
+
},
|
|
591
|
+
child: {
|
|
592
|
+
path: movedChildPage.path,
|
|
593
|
+
rank: movedChildPage.rank,
|
|
594
|
+
slug: movedChildPage.slug
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
const expected = {
|
|
598
|
+
bar: {
|
|
599
|
+
path: fooPage.path.concat('/', barPage.aposDocId),
|
|
600
|
+
rank: 0,
|
|
601
|
+
slug: '/foo/bar'
|
|
602
|
+
},
|
|
603
|
+
child: {
|
|
604
|
+
path: movedPage.path.concat('/', childPage.aposDocId),
|
|
605
|
+
rank: 0,
|
|
606
|
+
slug: '/foo/bar/child'
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
assert.deepEqual(actual, expected);
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
it('moving /foobar under /foo should wind up with /foo/foobar', async function() {
|
|
614
|
+
const fooPage = await apos.page.insert(
|
|
615
|
+
apos.task.getReq(),
|
|
616
|
+
'_home',
|
|
617
|
+
'lastChild',
|
|
618
|
+
{
|
|
619
|
+
slug: '/foo',
|
|
620
|
+
visibility: 'public',
|
|
621
|
+
type: 'test-page',
|
|
622
|
+
title: 'Foo Page'
|
|
623
|
+
}
|
|
624
|
+
);
|
|
625
|
+
const foobarPage = await apos.page.insert(
|
|
626
|
+
apos.task.getReq(),
|
|
627
|
+
'_home',
|
|
628
|
+
'lastChild',
|
|
629
|
+
{
|
|
630
|
+
slug: '/foobar',
|
|
631
|
+
visibility: 'public',
|
|
632
|
+
type: 'test-page',
|
|
633
|
+
title: 'Foobar Page'
|
|
634
|
+
}
|
|
635
|
+
);
|
|
636
|
+
const childPage = await apos.page.insert(
|
|
637
|
+
apos.task.getReq(),
|
|
638
|
+
foobarPage._id,
|
|
639
|
+
'lastChild',
|
|
640
|
+
{
|
|
641
|
+
slug: '/foobar/child',
|
|
642
|
+
visibility: 'public',
|
|
643
|
+
type: 'test-page',
|
|
644
|
+
title: 'Child Page'
|
|
645
|
+
}
|
|
646
|
+
);
|
|
647
|
+
|
|
648
|
+
await apos.page.move(
|
|
649
|
+
apos.task.getReq(),
|
|
650
|
+
foobarPage._id,
|
|
651
|
+
fooPage._id,
|
|
652
|
+
'lastChild'
|
|
653
|
+
);
|
|
654
|
+
|
|
655
|
+
const movedPage = await apos.page.find(apos.task.getAnonReq(), { _id: foobarPage._id }).toObject();
|
|
656
|
+
const movedChildPage = await apos.page.find(apos.task.getAnonReq(), { _id: childPage._id }).toObject();
|
|
657
|
+
|
|
658
|
+
const actual = {
|
|
659
|
+
foobar: {
|
|
660
|
+
path: movedPage.path,
|
|
661
|
+
rank: movedPage.rank,
|
|
662
|
+
slug: movedPage.slug
|
|
663
|
+
},
|
|
664
|
+
child: {
|
|
665
|
+
path: movedChildPage.path,
|
|
666
|
+
rank: movedChildPage.rank,
|
|
667
|
+
slug: movedChildPage.slug
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
const expected = {
|
|
671
|
+
foobar: {
|
|
672
|
+
path: fooPage.path.concat('/', foobarPage.aposDocId),
|
|
673
|
+
rank: 0,
|
|
674
|
+
slug: '/foo/foobar'
|
|
675
|
+
},
|
|
676
|
+
child: {
|
|
677
|
+
path: movedPage.path.concat('/', childPage.aposDocId),
|
|
678
|
+
rank: 0,
|
|
679
|
+
slug: '/foo/foobar/child'
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
assert.deepEqual(actual, expected);
|
|
684
|
+
});
|
|
685
|
+
});
|
|
686
|
+
|
|
460
687
|
it('inferred page relationships are correct', async function() {
|
|
461
688
|
const req = apos.task.getReq();
|
|
462
689
|
const pages = await apos.page.find(req, {}).toArray();
|
package/test/schemas.js
CHANGED
|
@@ -288,6 +288,44 @@ describe('Schemas', function() {
|
|
|
288
288
|
}
|
|
289
289
|
};
|
|
290
290
|
}
|
|
291
|
+
},
|
|
292
|
+
article: {
|
|
293
|
+
extend: '@apostrophecms/piece-type',
|
|
294
|
+
options: {
|
|
295
|
+
alias: 'article'
|
|
296
|
+
},
|
|
297
|
+
fields(self) {
|
|
298
|
+
return {
|
|
299
|
+
add: {
|
|
300
|
+
title: {
|
|
301
|
+
label: '',
|
|
302
|
+
type: 'string',
|
|
303
|
+
required: true
|
|
304
|
+
},
|
|
305
|
+
area: {
|
|
306
|
+
label: 'Area',
|
|
307
|
+
type: 'area',
|
|
308
|
+
options: {
|
|
309
|
+
widgets: {
|
|
310
|
+
'@apostrophecms/rich-text': {}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
array: {
|
|
315
|
+
label: 'Array',
|
|
316
|
+
type: 'array',
|
|
317
|
+
fields: {
|
|
318
|
+
add: {
|
|
319
|
+
arrayTitle: {
|
|
320
|
+
label: 'Array Title',
|
|
321
|
+
type: 'string'
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
}
|
|
291
329
|
}
|
|
292
330
|
}
|
|
293
331
|
});
|
|
@@ -2263,6 +2301,290 @@ describe('Schemas', function() {
|
|
|
2263
2301
|
assert(output.goodValue === '2022-05-09T22:36:00.000Z');
|
|
2264
2302
|
});
|
|
2265
2303
|
|
|
2304
|
+
it('should compare two document properly with the method getChanges', async function() {
|
|
2305
|
+
const req = apos.task.getReq();
|
|
2306
|
+
const instance = apos.article.newInstance();
|
|
2307
|
+
const article1 = {
|
|
2308
|
+
...instance,
|
|
2309
|
+
title: 'article 1',
|
|
2310
|
+
area: {
|
|
2311
|
+
_id: 'clrth36680007mnmd3jj7cta0',
|
|
2312
|
+
items: [
|
|
2313
|
+
{
|
|
2314
|
+
_id: 'clt79l48g001h2061j5ihxjkv',
|
|
2315
|
+
metaType: 'widget',
|
|
2316
|
+
type: '@apostrophecms/rich-text',
|
|
2317
|
+
aposPlaceholder: false,
|
|
2318
|
+
content: '<p>Some text here.</p>',
|
|
2319
|
+
permalinkIds: [],
|
|
2320
|
+
imageIds: []
|
|
2321
|
+
}
|
|
2322
|
+
],
|
|
2323
|
+
metaType: 'area'
|
|
2324
|
+
},
|
|
2325
|
+
array: [
|
|
2326
|
+
{
|
|
2327
|
+
_id: 'clt79llm800242061v4dx9kv5',
|
|
2328
|
+
metaType: 'arrayItem',
|
|
2329
|
+
scopedArrayName: 'doc.article.array',
|
|
2330
|
+
arrayTitle: 'array title 1'
|
|
2331
|
+
},
|
|
2332
|
+
{
|
|
2333
|
+
_id: 'clt79llm800242061v4d47364',
|
|
2334
|
+
metaType: 'arrayItem',
|
|
2335
|
+
scopedArrayName: 'doc.article.array',
|
|
2336
|
+
arrayTitle: 'array title 2'
|
|
2337
|
+
}
|
|
2338
|
+
]
|
|
2339
|
+
};
|
|
2340
|
+
const article2 = {
|
|
2341
|
+
...article1,
|
|
2342
|
+
title: 'article 2'
|
|
2343
|
+
};
|
|
2344
|
+
const article3 = {
|
|
2345
|
+
...instance,
|
|
2346
|
+
title: 'article 3',
|
|
2347
|
+
|
|
2348
|
+
area: {
|
|
2349
|
+
_id: 'clrth36680007mnmd3jj7cta0',
|
|
2350
|
+
items: [
|
|
2351
|
+
{
|
|
2352
|
+
_id: 'clt79l48g001h2061j5ihxjkv',
|
|
2353
|
+
metaType: 'widget',
|
|
2354
|
+
type: '@apostrophecms/rich-text',
|
|
2355
|
+
aposPlaceholder: false,
|
|
2356
|
+
content: '<p>Some text here changed.</p>',
|
|
2357
|
+
permalinkIds: [],
|
|
2358
|
+
imageIds: []
|
|
2359
|
+
}
|
|
2360
|
+
],
|
|
2361
|
+
metaType: 'area'
|
|
2362
|
+
},
|
|
2363
|
+
array: [
|
|
2364
|
+
{
|
|
2365
|
+
_id: 'clt79llm800242061v4dx9kv5',
|
|
2366
|
+
metaType: 'arrayItem',
|
|
2367
|
+
scopedArrayName: 'doc.article.array',
|
|
2368
|
+
arrayTitle: 'array title 1 changed'
|
|
2369
|
+
},
|
|
2370
|
+
{
|
|
2371
|
+
_id: 'clt79llm800242061v4d47364',
|
|
2372
|
+
metaType: 'arrayItem',
|
|
2373
|
+
scopedArrayName: 'doc.article.array',
|
|
2374
|
+
arrayTitle: 'array title 2'
|
|
2375
|
+
}
|
|
2376
|
+
]
|
|
2377
|
+
};
|
|
2378
|
+
|
|
2379
|
+
await apos.article.insert(req, article1);
|
|
2380
|
+
await apos.article.insert(req, article2);
|
|
2381
|
+
await apos.article.insert(req, article3);
|
|
2382
|
+
|
|
2383
|
+
const art1 = await apos.doc.db.findOne({ title: 'article 1' });
|
|
2384
|
+
const art2 = await apos.doc.db.findOne({ title: 'article 2' });
|
|
2385
|
+
const art3 = await apos.doc.db.findOne({ title: 'article 3' });
|
|
2386
|
+
|
|
2387
|
+
const changes11 = apos.schema.getChanges(req, apos.article.schema, art1, art1);
|
|
2388
|
+
const changes12 = apos.schema.getChanges(req, apos.article.schema, art1, art2);
|
|
2389
|
+
const changes23 = apos.schema.getChanges(req, apos.article.schema, art2, art3);
|
|
2390
|
+
const actual = {
|
|
2391
|
+
changes11,
|
|
2392
|
+
changes12,
|
|
2393
|
+
changes23
|
|
2394
|
+
};
|
|
2395
|
+
const expected = {
|
|
2396
|
+
changes11: [],
|
|
2397
|
+
changes12: [ 'title', 'slug' ],
|
|
2398
|
+
changes23: [ 'title', 'slug', 'area', 'array' ]
|
|
2399
|
+
};
|
|
2400
|
+
|
|
2401
|
+
assert.deepEqual(actual, expected);
|
|
2402
|
+
});
|
|
2403
|
+
|
|
2404
|
+
describe('field.readOnly with default value', function() {
|
|
2405
|
+
const givenSchema = [
|
|
2406
|
+
{
|
|
2407
|
+
name: 'title',
|
|
2408
|
+
type: 'string'
|
|
2409
|
+
},
|
|
2410
|
+
{
|
|
2411
|
+
name: 'array',
|
|
2412
|
+
type: 'array',
|
|
2413
|
+
schema: [
|
|
2414
|
+
{
|
|
2415
|
+
name: 'planet',
|
|
2416
|
+
type: 'string',
|
|
2417
|
+
def: 'Earth',
|
|
2418
|
+
readOnly: true
|
|
2419
|
+
},
|
|
2420
|
+
{
|
|
2421
|
+
name: 'moon',
|
|
2422
|
+
type: 'string'
|
|
2423
|
+
}
|
|
2424
|
+
]
|
|
2425
|
+
},
|
|
2426
|
+
{
|
|
2427
|
+
name: 'object',
|
|
2428
|
+
type: 'object',
|
|
2429
|
+
schema: [
|
|
2430
|
+
{
|
|
2431
|
+
name: 'planet',
|
|
2432
|
+
type: 'string',
|
|
2433
|
+
def: 'Earth',
|
|
2434
|
+
readOnly: true
|
|
2435
|
+
},
|
|
2436
|
+
{
|
|
2437
|
+
name: 'moon',
|
|
2438
|
+
type: 'string'
|
|
2439
|
+
}
|
|
2440
|
+
]
|
|
2441
|
+
},
|
|
2442
|
+
{
|
|
2443
|
+
name: '_relationship',
|
|
2444
|
+
type: 'relationship',
|
|
2445
|
+
limit: 1,
|
|
2446
|
+
withType: '@apostrophecms/any-page-type',
|
|
2447
|
+
label: 'Page Title',
|
|
2448
|
+
idsStorage: 'pageId',
|
|
2449
|
+
schema: [
|
|
2450
|
+
{
|
|
2451
|
+
name: 'planet',
|
|
2452
|
+
type: 'string',
|
|
2453
|
+
def: 'Earth',
|
|
2454
|
+
readOnly: true
|
|
2455
|
+
},
|
|
2456
|
+
{
|
|
2457
|
+
name: 'moon',
|
|
2458
|
+
type: 'string'
|
|
2459
|
+
}
|
|
2460
|
+
]
|
|
2461
|
+
}
|
|
2462
|
+
];
|
|
2463
|
+
|
|
2464
|
+
it('should keep read only values when editing a document', async function() {
|
|
2465
|
+
const req = apos.task.getReq();
|
|
2466
|
+
const schema = apos.schema.compose({
|
|
2467
|
+
addFields: givenSchema
|
|
2468
|
+
});
|
|
2469
|
+
const home = await apos.page.find(req, { slug: '/' }).toObject();
|
|
2470
|
+
|
|
2471
|
+
const data = {
|
|
2472
|
+
_relationship: [
|
|
2473
|
+
{
|
|
2474
|
+
...home,
|
|
2475
|
+
_fields: {
|
|
2476
|
+
planet: 'Saturn',
|
|
2477
|
+
moon: 'Titan'
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
],
|
|
2481
|
+
array: [
|
|
2482
|
+
{
|
|
2483
|
+
_id: 'Jupiter-Io',
|
|
2484
|
+
moon: 'Io'
|
|
2485
|
+
},
|
|
2486
|
+
{
|
|
2487
|
+
_id: 'Mars-Phobos',
|
|
2488
|
+
moon: 'Phobos'
|
|
2489
|
+
}
|
|
2490
|
+
],
|
|
2491
|
+
object: {
|
|
2492
|
+
_id: 'Neptune-Triton',
|
|
2493
|
+
moon: 'Triton'
|
|
2494
|
+
},
|
|
2495
|
+
pageId: [ home._id ],
|
|
2496
|
+
pageFields: {
|
|
2497
|
+
[home._id]: {
|
|
2498
|
+
planet: 'Saturn'
|
|
2499
|
+
}
|
|
2500
|
+
},
|
|
2501
|
+
title: 'Sol'
|
|
2502
|
+
};
|
|
2503
|
+
const destination = {
|
|
2504
|
+
_relationship: [
|
|
2505
|
+
{
|
|
2506
|
+
...home,
|
|
2507
|
+
_fields: {
|
|
2508
|
+
planet: 'Saturn'
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
],
|
|
2512
|
+
array: [
|
|
2513
|
+
{
|
|
2514
|
+
_id: 'Jupiter-Io',
|
|
2515
|
+
planet: 'Jupiter'
|
|
2516
|
+
},
|
|
2517
|
+
{
|
|
2518
|
+
_id: 'Mars-Phobos',
|
|
2519
|
+
planet: 'Mars'
|
|
2520
|
+
}
|
|
2521
|
+
],
|
|
2522
|
+
object: {
|
|
2523
|
+
_id: 'Neptune-Triton',
|
|
2524
|
+
planet: 'Neptune'
|
|
2525
|
+
},
|
|
2526
|
+
pageId: [ home._id ],
|
|
2527
|
+
pageFields: {
|
|
2528
|
+
[home._id]: {
|
|
2529
|
+
planet: 'Saturn'
|
|
2530
|
+
}
|
|
2531
|
+
},
|
|
2532
|
+
title: 'Default'
|
|
2533
|
+
};
|
|
2534
|
+
await apos.schema.convert(
|
|
2535
|
+
req,
|
|
2536
|
+
schema,
|
|
2537
|
+
data,
|
|
2538
|
+
destination
|
|
2539
|
+
);
|
|
2540
|
+
|
|
2541
|
+
const actual = destination;
|
|
2542
|
+
const expected = {
|
|
2543
|
+
_relationship: [
|
|
2544
|
+
{
|
|
2545
|
+
_fields: {
|
|
2546
|
+
planet: 'Saturn',
|
|
2547
|
+
moon: 'Titan'
|
|
2548
|
+
},
|
|
2549
|
+
...home
|
|
2550
|
+
}
|
|
2551
|
+
],
|
|
2552
|
+
array: [
|
|
2553
|
+
{
|
|
2554
|
+
_id: 'Jupiter-Io',
|
|
2555
|
+
metaType: 'arrayItem',
|
|
2556
|
+
moon: 'Io',
|
|
2557
|
+
planet: 'Jupiter',
|
|
2558
|
+
scopedArrayName: undefined
|
|
2559
|
+
},
|
|
2560
|
+
{
|
|
2561
|
+
_id: 'Mars-Phobos',
|
|
2562
|
+
metaType: 'arrayItem',
|
|
2563
|
+
moon: 'Phobos',
|
|
2564
|
+
planet: 'Mars',
|
|
2565
|
+
scopedArrayName: undefined
|
|
2566
|
+
}
|
|
2567
|
+
],
|
|
2568
|
+
object: {
|
|
2569
|
+
_id: 'Neptune-Triton',
|
|
2570
|
+
metaType: 'objectItem',
|
|
2571
|
+
moon: 'Triton',
|
|
2572
|
+
planet: 'Neptune',
|
|
2573
|
+
scopedObjectName: undefined
|
|
2574
|
+
},
|
|
2575
|
+
pageId: [ home._id ],
|
|
2576
|
+
pageFields: {
|
|
2577
|
+
[home._id]: {
|
|
2578
|
+
planet: 'Saturn'
|
|
2579
|
+
}
|
|
2580
|
+
},
|
|
2581
|
+
title: 'Sol'
|
|
2582
|
+
};
|
|
2583
|
+
|
|
2584
|
+
assert.deepEqual(actual, expected);
|
|
2585
|
+
});
|
|
2586
|
+
});
|
|
2587
|
+
|
|
2266
2588
|
describe('field editPermission|viewPermission', function() {
|
|
2267
2589
|
const schema = [
|
|
2268
2590
|
{
|