apostrophe 3.37.0 → 3.38.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 +19 -3
- package/modules/@apostrophecms/attachment/index.js +3 -0
- package/modules/@apostrophecms/doc/index.js +15 -0
- package/modules/@apostrophecms/doc-type/index.js +1 -0
- package/modules/@apostrophecms/i18n/index.js +16 -6
- package/modules/@apostrophecms/image/ui/apos/components/AposMediaManager.vue +15 -0
- package/modules/@apostrophecms/modal/ui/apos/mixins/AposDocsManagerMixin.js +2 -1
- package/modules/@apostrophecms/notification/index.js +16 -1
- package/modules/@apostrophecms/page/index.js +9 -1
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputRelationship.vue +24 -0
- package/modules/@apostrophecms/user/index.js +3 -0
- package/package.json +1 -1
- package/test/draft-published.js +9 -4
- package/test/modules/test-page/views/page.html +2 -0
- package/test/pages.js +18 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.38.0 (2023-01-18)
|
|
4
|
+
|
|
5
|
+
### Adds
|
|
6
|
+
|
|
7
|
+
* Emit a `beforeSave` event from the `@apostrophecms:notification` module, with `req` and the `notification` as arguments, in order to give the possibility to override the notification.
|
|
8
|
+
* Emit a `beforeInsert` event from the `@apostrophecms:attachment` module, with `req` and the `doc` as arguments, in order to give the possibility to override the attachment.
|
|
9
|
+
* Emit a `beforeSaveSafe` event from the `@apostrophecms:user` module, with `req`, `safeUser` and `user` as arguments, in order to give the possibility to override properties of the `safeUser` object which contains password hashes and other information too sensitive to be stored in the aposDocs collection.
|
|
10
|
+
* Automatically convert failed uppercase URLs to their lowercase version - can be disabled with `redirectFailedUpperCaseUrls: false` in `@apostrophecms/page/index.js` options. This only comes into play if a 404 is about to happen.
|
|
11
|
+
* Automatically convert country codes in locales like `xx-yy` to `xx-YY` before passing them to `i18next`, which is strict about uppercase country codes.
|
|
12
|
+
|
|
13
|
+
### Fixes
|
|
14
|
+
|
|
15
|
+
* Documents kept as the `previous` version for undo purposes were not properly marked as such, breaking the public language switcher in some cases. This was fixed and a migration was added for existing data.
|
|
16
|
+
* Uploading an image in an apostrophe area with `minSize` requirements will not trigger an unexpected error anymore. If the image is too small, a notification will be displayed with the minimum size requirements. The `Edit Image` modal will now display the minimum size requirements, if any, above the `Browse Images` field.
|
|
17
|
+
* Some browsers saw the empty `POST` response for new notifications as invalid XML. It will now return an empty JSON object with the `Content-Type` set to `application/json`.
|
|
18
|
+
|
|
3
19
|
## 3.37.0 (2023-01-06)
|
|
4
20
|
|
|
5
21
|
### Adds
|
|
@@ -15,14 +31,14 @@
|
|
|
15
31
|
e.g. `A permission.can() call was made with a type that has no manager: @apostrophecms/polymorphic-type`.
|
|
16
32
|
* The module `webpack.extensions` configuration is not applied to the core Admin UI build anymore. This is the correct and intended behavior as explained in the [relevant documentation](https://v3.docs.apostrophecms.org/guide/webpack.html#extending-webpack-configuration).
|
|
17
33
|
* The `previewImage` option now works properly for widget modules loaded from npm and those that subclass them. Specifically, the preview image may be provided in the `public/` subdirectory of the original module, the project-level configuration of it, or a subclass.
|
|
18
|
-
|
|
34
|
+
|
|
19
35
|
## 3.36.0 (2022-12-22)
|
|
20
36
|
|
|
21
37
|
### Adds
|
|
22
38
|
|
|
23
|
-
* `shortcut` option for piece modules, allowing easy re-mapping of the manager command shortcut per module.
|
|
39
|
+
* `shortcut` option for piece modules, allowing easy re-mapping of the manager command shortcut per module.
|
|
24
40
|
|
|
25
|
-
### Fixes
|
|
41
|
+
### Fixes
|
|
26
42
|
|
|
27
43
|
* Ensure there are no conflicting command shortcuts for the core modules.
|
|
28
44
|
|
|
@@ -443,6 +443,9 @@ module.exports = {
|
|
|
443
443
|
await Promise.promisify(self.uploadfs.copyIn)(file.path, '/attachments/' + info._id + '-' + info.name + '.' + info.extension);
|
|
444
444
|
}
|
|
445
445
|
info.createdAt = new Date();
|
|
446
|
+
|
|
447
|
+
await self.emit('beforeInsert', req, info);
|
|
448
|
+
|
|
446
449
|
await self.db.insertOne(info);
|
|
447
450
|
return info;
|
|
448
451
|
},
|
|
@@ -37,6 +37,7 @@ module.exports = {
|
|
|
37
37
|
await self.createIndexes();
|
|
38
38
|
self.addLegacyMigrations();
|
|
39
39
|
self.addCacheFieldMigration();
|
|
40
|
+
self.addSetPreviousDocsAposModeMigration();
|
|
40
41
|
},
|
|
41
42
|
restApiRoutes(self) {
|
|
42
43
|
return {
|
|
@@ -1288,6 +1289,20 @@ module.exports = {
|
|
|
1288
1289
|
addCacheFieldMigration() {
|
|
1289
1290
|
self.apos.migration.add('add-cache-invalidated-at-field', self.setCacheField);
|
|
1290
1291
|
},
|
|
1292
|
+
|
|
1293
|
+
async addSetPreviousDocsAposModeMigration () {
|
|
1294
|
+
self.apos.migration.add('set-previous-docs-apos-mode', async () => {
|
|
1295
|
+
await self.apos.doc.db.updateMany({
|
|
1296
|
+
_id: { $regex: ':previous$' },
|
|
1297
|
+
aposMode: { $ne: 'previous' }
|
|
1298
|
+
}, {
|
|
1299
|
+
$set: {
|
|
1300
|
+
aposMode: 'previous'
|
|
1301
|
+
}
|
|
1302
|
+
});
|
|
1303
|
+
});
|
|
1304
|
+
},
|
|
1305
|
+
|
|
1291
1306
|
...require('./lib/legacy-migrations')(self)
|
|
1292
1307
|
};
|
|
1293
1308
|
}
|
|
@@ -847,6 +847,7 @@ module.exports = {
|
|
|
847
847
|
if (previousPublished) {
|
|
848
848
|
previousPublished._id = previousPublished._id.replace(':published', ':previous');
|
|
849
849
|
previousPublished.aposLocale = previousPublished.aposLocale.replace(':published', ':previous');
|
|
850
|
+
previousPublished.aposMode = 'previous';
|
|
850
851
|
Object.assign(previousPublished, await self.getDeduplicationSet(req, previousPublished));
|
|
851
852
|
await self.apos.doc.db.replaceOne({
|
|
852
853
|
_id: previousPublished._id
|
|
@@ -409,7 +409,11 @@ module.exports = {
|
|
|
409
409
|
continue;
|
|
410
410
|
}
|
|
411
411
|
const data = JSON.parse(fs.readFileSync(path.join(localizationsDir, localizationFile)));
|
|
412
|
-
|
|
412
|
+
|
|
413
|
+
// enforce i18next locale format as xx-XX
|
|
414
|
+
const [ language, country ] = localizationFile.replace('.json', '').split('-');
|
|
415
|
+
const locale = country ? `${language.toLocaleLowerCase()}-${country.toUpperCase()}` : language;
|
|
416
|
+
|
|
413
417
|
self.i18next.addResourceBundle(locale, ns, data, true, true);
|
|
414
418
|
}
|
|
415
419
|
}
|
|
@@ -566,11 +570,17 @@ module.exports = {
|
|
|
566
570
|
return i18n;
|
|
567
571
|
},
|
|
568
572
|
getLocales() {
|
|
569
|
-
const locales = self.options.locales
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
573
|
+
const locales = self.options.locales
|
|
574
|
+
? Object.fromEntries(Object.entries(self.options.locales).map(([ name, options ]) => {
|
|
575
|
+
// enforce i18next locale format as xx-XX
|
|
576
|
+
const [ language, country ] = name.split('-');
|
|
577
|
+
return country ? [ `${language.toLocaleLowerCase()}-${country.toUpperCase()}`, options ] : [ language, options ];
|
|
578
|
+
}))
|
|
579
|
+
: {
|
|
580
|
+
en: {
|
|
581
|
+
label: 'English'
|
|
582
|
+
}
|
|
583
|
+
};
|
|
574
584
|
verifyLocales(locales, self.apos.options.baseUrl);
|
|
575
585
|
return locales;
|
|
576
586
|
},
|
|
@@ -318,6 +318,21 @@ export default {
|
|
|
318
318
|
this.uploading = false;
|
|
319
319
|
await this.getMedia();
|
|
320
320
|
|
|
321
|
+
if (Array.isArray(imgIds) && imgIds.length && this.items.length === 0) {
|
|
322
|
+
const [ widgetOptions = {} ] = apos.area.widgetOptions;
|
|
323
|
+
const [ width, height ] = widgetOptions.minSize || [];
|
|
324
|
+
await apos.notify('apostrophe:minSize', {
|
|
325
|
+
type: 'danger',
|
|
326
|
+
icon: 'alert-circle-icon',
|
|
327
|
+
dismiss: true,
|
|
328
|
+
interpolate: {
|
|
329
|
+
width,
|
|
330
|
+
height
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
this.updateEditing(null);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
321
336
|
if (Array.isArray(imgIds) && imgIds.length) {
|
|
322
337
|
this.checked = this.checked.concat(imgIds);
|
|
323
338
|
|
|
@@ -219,7 +219,8 @@ export default {
|
|
|
219
219
|
// database.
|
|
220
220
|
this.checked.forEach(id => {
|
|
221
221
|
if (this.checkedDocs.findIndex(doc => doc._id === id) === -1) {
|
|
222
|
-
this.
|
|
222
|
+
const found = this.items.find(item => item._id === id);
|
|
223
|
+
found && this.checkedDocs.push(found);
|
|
223
224
|
}
|
|
224
225
|
});
|
|
225
226
|
|
|
@@ -92,6 +92,7 @@ module.exports = {
|
|
|
92
92
|
async post(req) {
|
|
93
93
|
const type = self.apos.launder.select(req.body.type, [
|
|
94
94
|
'danger',
|
|
95
|
+
'error',
|
|
95
96
|
'warning',
|
|
96
97
|
'success',
|
|
97
98
|
'info'
|
|
@@ -143,9 +144,14 @@ module.exports = {
|
|
|
143
144
|
put(req, _id) {
|
|
144
145
|
throw self.apos.error('unimplemented');
|
|
145
146
|
},
|
|
146
|
-
patch(req, _id) {
|
|
147
|
+
async patch(req, _id) {
|
|
147
148
|
const dismissed = self.apos.launder.boolean(req.body.dismissed);
|
|
148
149
|
if (dismissed) {
|
|
150
|
+
await self.emit('beforeSave', req, {
|
|
151
|
+
_id,
|
|
152
|
+
dismissed
|
|
153
|
+
});
|
|
154
|
+
|
|
149
155
|
return self.db.updateOne({ _id }, {
|
|
150
156
|
$set: {
|
|
151
157
|
dismissed
|
|
@@ -283,6 +289,8 @@ module.exports = {
|
|
|
283
289
|
|
|
284
290
|
Object.assign(notification, copiedOptions);
|
|
285
291
|
|
|
292
|
+
await self.emit('beforeSave', req, notification);
|
|
293
|
+
|
|
286
294
|
// We await here rather than returning because we expressly do not
|
|
287
295
|
// want to leak mongodb metadata to the browser
|
|
288
296
|
await self.db.updateOne(
|
|
@@ -302,6 +310,8 @@ module.exports = {
|
|
|
302
310
|
noteId: notification._id
|
|
303
311
|
};
|
|
304
312
|
}
|
|
313
|
+
|
|
314
|
+
return {};
|
|
305
315
|
},
|
|
306
316
|
|
|
307
317
|
// The dismiss method accepts the following arguments:
|
|
@@ -317,6 +327,11 @@ module.exports = {
|
|
|
317
327
|
await pause(delay);
|
|
318
328
|
|
|
319
329
|
try {
|
|
330
|
+
await self.emit('beforeSave', req, {
|
|
331
|
+
_id: noteId,
|
|
332
|
+
dismissed: true
|
|
333
|
+
});
|
|
334
|
+
|
|
320
335
|
await self.db.updateOne(
|
|
321
336
|
{
|
|
322
337
|
_id: noteId
|
|
@@ -33,7 +33,8 @@ module.exports = {
|
|
|
33
33
|
orphan: true,
|
|
34
34
|
title: 'Archive'
|
|
35
35
|
}
|
|
36
|
-
]
|
|
36
|
+
],
|
|
37
|
+
redirectFailedUpperCaseUrls: true
|
|
37
38
|
},
|
|
38
39
|
batchOperations: {
|
|
39
40
|
add: {
|
|
@@ -1565,6 +1566,7 @@ database.`);
|
|
|
1565
1566
|
if (self.isFound(req)) {
|
|
1566
1567
|
return;
|
|
1567
1568
|
}
|
|
1569
|
+
|
|
1568
1570
|
if (req.user && (req.mode === 'published')) {
|
|
1569
1571
|
// Try again in draft mode
|
|
1570
1572
|
try {
|
|
@@ -1613,6 +1615,12 @@ database.`);
|
|
|
1613
1615
|
// Nonfatal, we were just probing
|
|
1614
1616
|
}
|
|
1615
1617
|
}
|
|
1618
|
+
|
|
1619
|
+
// If uppercase letters in URL, try with lowercase
|
|
1620
|
+
if (self.options.redirectFailedUpperCaseUrls && /[A-Z]/.test(req.path)) {
|
|
1621
|
+
req.redirect = self.apos.url.build(req.path.toLowerCase(), req.query);
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1616
1624
|
// Give all modules a chance to save the day
|
|
1617
1625
|
await self.emit('notFound', req);
|
|
1618
1626
|
// Are we happy now?
|
|
@@ -6,6 +6,17 @@
|
|
|
6
6
|
:modifiers="modifiers"
|
|
7
7
|
>
|
|
8
8
|
<template #additional>
|
|
9
|
+
<div
|
|
10
|
+
v-if="minSize[0] || minSize[1]"
|
|
11
|
+
class="apos-field__min-size"
|
|
12
|
+
>
|
|
13
|
+
{{
|
|
14
|
+
$t('apostrophe:minSize', {
|
|
15
|
+
width: minSize[0] || '???',
|
|
16
|
+
height: minSize[1] || '???'
|
|
17
|
+
})
|
|
18
|
+
}}
|
|
19
|
+
</div>
|
|
9
20
|
<AposMinMaxCount
|
|
10
21
|
:field="field"
|
|
11
22
|
:value="next"
|
|
@@ -122,6 +133,11 @@ export default {
|
|
|
122
133
|
modifiers.push('block');
|
|
123
134
|
}
|
|
124
135
|
return modifiers;
|
|
136
|
+
},
|
|
137
|
+
minSize() {
|
|
138
|
+
const [ widgetOptions = {} ] = apos.area.widgetOptions;
|
|
139
|
+
|
|
140
|
+
return widgetOptions.minSize || [];
|
|
125
141
|
}
|
|
126
142
|
},
|
|
127
143
|
watch: {
|
|
@@ -287,4 +303,12 @@ export default {
|
|
|
287
303
|
padding: 0;
|
|
288
304
|
}
|
|
289
305
|
}
|
|
306
|
+
|
|
307
|
+
.apos-field__min-size {
|
|
308
|
+
@include type-help;
|
|
309
|
+
display: flex;
|
|
310
|
+
flex-grow: 1;
|
|
311
|
+
margin-bottom: $spacing-base;
|
|
312
|
+
font-weight: var(--a-weight-bold);
|
|
313
|
+
}
|
|
290
314
|
</style>
|
|
@@ -292,6 +292,9 @@ module.exports = {
|
|
|
292
292
|
|
|
293
293
|
await self.hashPassword(doc, safeUser);
|
|
294
294
|
await self.hashSecrets(doc, safeUser);
|
|
295
|
+
|
|
296
|
+
await self.emit('beforeSaveSafe', req, safeUser, doc);
|
|
297
|
+
|
|
295
298
|
if (action === 'insert') {
|
|
296
299
|
await self.safe.insertOne(safeUser);
|
|
297
300
|
} else {
|
package/package.json
CHANGED
package/test/draft-published.js
CHANGED
|
@@ -209,11 +209,12 @@ describe('Draft / Published', function() {
|
|
|
209
209
|
}), testDraftProduct);
|
|
210
210
|
});
|
|
211
211
|
|
|
212
|
-
it('"previous published" should be deduplicated at this point', async function() {
|
|
212
|
+
it('"previous published" should be deduplicated at this point, and previous have the right props', async function() {
|
|
213
213
|
const previous = await apos.doc.db.findOne({
|
|
214
214
|
_id: testDraftProduct._id.replace(':draft', ':previous')
|
|
215
215
|
});
|
|
216
216
|
assert(previous);
|
|
217
|
+
assert(previous.aposMode === 'previous');
|
|
217
218
|
assert.strictEqual(previous.slug, `deduplicate-${previous.aposDocId}-test-product`);
|
|
218
219
|
});
|
|
219
220
|
|
|
@@ -539,6 +540,7 @@ describe('Draft / Published', function() {
|
|
|
539
540
|
}), {
|
|
540
541
|
aposDocId: sibling.aposDocId
|
|
541
542
|
}).children(true).toObject();
|
|
543
|
+
|
|
542
544
|
assert(siblingPublished && siblingPublished._children && siblingPublished._children[0] && siblingPublished._children[0]._id === grandchild._id.replace(':draft', ':published'));
|
|
543
545
|
});
|
|
544
546
|
|
|
@@ -556,17 +558,20 @@ describe('Draft / Published', function() {
|
|
|
556
558
|
const draftItem = {
|
|
557
559
|
...baseItem,
|
|
558
560
|
_id: 'some-page:en:draft',
|
|
559
|
-
aposLocale: 'en:draft'
|
|
561
|
+
aposLocale: 'en:draft',
|
|
562
|
+
aposMode: 'draft'
|
|
560
563
|
};
|
|
561
564
|
const publishedItem = {
|
|
562
565
|
...baseItem,
|
|
563
566
|
_id: 'some-page:en:published',
|
|
564
|
-
aposLocale: 'en:published'
|
|
567
|
+
aposLocale: 'en:published',
|
|
568
|
+
aposMode: 'published'
|
|
565
569
|
};
|
|
566
570
|
const previousItem = {
|
|
567
571
|
...baseItem,
|
|
568
572
|
_id: 'some-page:en:previous',
|
|
569
|
-
aposLocale: 'en:previous'
|
|
573
|
+
aposLocale: 'en:previous',
|
|
574
|
+
aposMode: 'previous'
|
|
570
575
|
};
|
|
571
576
|
|
|
572
577
|
let draft;
|
package/test/pages.js
CHANGED
|
@@ -215,6 +215,24 @@ describe('Pages', function() {
|
|
|
215
215
|
assert(page.path === `${homeId.replace(':en:published', '')}/parent/child`);
|
|
216
216
|
});
|
|
217
217
|
|
|
218
|
+
it('should convert an uppercase URL to its lowercase version', async function() {
|
|
219
|
+
const response = await apos.http.get('/PArent/cHild', {
|
|
220
|
+
fullResponse: true
|
|
221
|
+
});
|
|
222
|
+
assert(response.body.match(/URL: \/parent\/child/));
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should NOT convert an uppercase URL if redirectFailedUpperCaseUrls is false', async function() {
|
|
226
|
+
apos.page.options.redirectFailedUpperCaseUrls = false;
|
|
227
|
+
try {
|
|
228
|
+
await apos.http.get('/PArent/cHild', {
|
|
229
|
+
fullResponse: true
|
|
230
|
+
});
|
|
231
|
+
} catch (error) {
|
|
232
|
+
assert(error.status === 404);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
218
236
|
it('should be able to include the ancestors of a page', async function() {
|
|
219
237
|
const cursor = apos.page.find(apos.task.getAnonReq(), { slug: '/parent/child' });
|
|
220
238
|
|
|
@@ -1151,8 +1169,6 @@ describe('Pages', function() {
|
|
|
1151
1169
|
|
|
1152
1170
|
it('should grant public access to a draft after having enabled draft sharing', async function() {
|
|
1153
1171
|
const publicUrl = generatePublicUrl(shareResponse);
|
|
1154
|
-
console.log('publicUrl', publicUrl);
|
|
1155
|
-
|
|
1156
1172
|
const response = await apos.http.get(shareResponse._url, { fullResponse: true });
|
|
1157
1173
|
const publicResponse = await apos.http.get(publicUrl, { fullResponse: true });
|
|
1158
1174
|
|