apostrophe 3.65.0 → 3.66.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 +10 -7
- package/modules/@apostrophecms/i18n/i18n/en.json +2 -0
- package/modules/@apostrophecms/image/index.js +185 -20
- package/modules/@apostrophecms/page/index.js +1 -1
- package/modules/@apostrophecms/schema/index.js +33 -8
- 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/CHANGELOG.md
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 3.
|
|
3
|
+
## 3.66.0 (2024-05-15)
|
|
4
4
|
|
|
5
|
-
###
|
|
5
|
+
### Fixes
|
|
6
|
+
|
|
7
|
+
* Autocrop image attachments for referenced documents when replacing an image in the Media Manager.
|
|
8
|
+
* Backports some internal A4 UI logic for metadata to make the new `document-versions` comparison feature compatible with A3.
|
|
9
|
+
|
|
10
|
+
# Changelog
|
|
6
11
|
|
|
7
|
-
|
|
12
|
+
## 3.65.0 (2024-05-06)
|
|
13
|
+
|
|
14
|
+
* 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
15
|
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
16
|
Thanks to Michelin for contributing this feature.
|
|
10
17
|
|
|
11
18
|
## 3.64.0 (2024-04-18)
|
|
12
19
|
|
|
13
|
-
### Adds
|
|
14
|
-
|
|
15
20
|
### Fixes
|
|
16
21
|
|
|
17
22
|
* Add the missing `metaType` property to newly inserted widgets.
|
|
@@ -42,8 +47,6 @@ that these keys would be present.
|
|
|
42
47
|
* `field.help` and `field.htmlHelp` are now correctly translated when displayed in a tooltip.
|
|
43
48
|
This was also an expectation for the multisite module.
|
|
44
49
|
|
|
45
|
-
## UNRELEASED
|
|
46
|
-
|
|
47
50
|
### Adds
|
|
48
51
|
|
|
49
52
|
* 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
|
};
|
|
@@ -2300,7 +2300,7 @@ database.`);
|
|
|
2300
2300
|
// Apostrophe queries used to fetch Apostrophe pages
|
|
2301
2301
|
// consult this method.
|
|
2302
2302
|
getBaseUrl(req) {
|
|
2303
|
-
const hostname = self.apos.i18n.locales[req.locale]
|
|
2303
|
+
const hostname = self.apos.i18n.locales[req.locale]?.hostname;
|
|
2304
2304
|
|
|
2305
2305
|
return hostname
|
|
2306
2306
|
? `${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
|
|
@@ -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);
|