apostrophe 4.6.1 → 4.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/main.yml +1 -1
- package/CHANGELOG.md +39 -1
- package/lib/big-upload-client.js +100 -0
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBar.vue +5 -3
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBarLocale.vue +6 -3
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBarUser.vue +4 -1
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +24 -16
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextTitle.vue +1 -0
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposSavingIndicator.vue +7 -5
- package/modules/@apostrophecms/area/index.js +5 -2
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaContextualMenu.vue +20 -12
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaExpandedMenu.vue +11 -7
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaMenu.vue +20 -12
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaMenuItem.vue +3 -1
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +15 -11
- package/modules/@apostrophecms/attachment/index.js +4 -2
- package/modules/@apostrophecms/command-menu/ui/apos/components/AposCommandMenuKey.vue +28 -24
- package/modules/@apostrophecms/command-menu/ui/apos/components/AposCommandMenuShortcut.vue +17 -11
- package/modules/@apostrophecms/doc/index.js +22 -19
- package/modules/@apostrophecms/doc-type/index.js +6 -2
- package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +9 -5
- package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocLocalePicker.vue +10 -5
- package/modules/@apostrophecms/doc-type/ui/apos/logic/AposDocContextMenu.js +12 -0
- package/modules/@apostrophecms/http/index.js +19 -3
- package/modules/@apostrophecms/http/lib/big-upload-middleware.js +251 -0
- package/modules/@apostrophecms/i18n/i18n/de.json +1 -1
- package/modules/@apostrophecms/i18n/i18n/en.json +9 -1
- package/modules/@apostrophecms/i18n/i18n/es.json +1 -1
- package/modules/@apostrophecms/i18n/i18n/fr.json +1 -1
- package/modules/@apostrophecms/i18n/i18n/it.json +1 -1
- package/modules/@apostrophecms/i18n/i18n/pt-BR.json +1 -1
- package/modules/@apostrophecms/i18n/i18n/sk.json +1 -1
- package/modules/@apostrophecms/i18n/index.js +3 -0
- package/modules/@apostrophecms/i18n/ui/apos/components/AposI18nLocalize.vue +30 -16
- package/modules/@apostrophecms/i18n/ui/apos/components/AposI18nLocalizeErrors.vue +7 -5
- package/modules/@apostrophecms/image/ui/apos/components/AposImageCropper.vue +5 -1
- package/modules/@apostrophecms/image/ui/apos/components/AposImageRelationshipEditor.vue +10 -6
- package/modules/@apostrophecms/image/ui/apos/components/AposMediaManager.vue +40 -18
- package/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerDisplay.vue +35 -25
- package/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerEditor.vue +11 -5
- package/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerSelections.vue +15 -9
- package/modules/@apostrophecms/image/ui/apos/components/AposMediaUploader.vue +39 -31
- package/modules/@apostrophecms/job/index.js +1 -1
- package/modules/@apostrophecms/login/ui/apos/components/AposLoginForm.vue +9 -7
- package/modules/@apostrophecms/login/ui/apos/components/TheAposLogin.vue +17 -13
- package/modules/@apostrophecms/login/ui/apos/components/TheAposLoginHeader.vue +30 -20
- package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +5 -0
- package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +4 -1
- package/modules/@apostrophecms/modal/ui/apos/components/AposModalBreadcrumbs.vue +8 -4
- package/modules/@apostrophecms/modal/ui/apos/components/AposModalConfirm.vue +14 -8
- package/modules/@apostrophecms/modal/ui/apos/components/AposModalShareDraft.vue +32 -22
- package/modules/@apostrophecms/modal/ui/apos/components/AposModalTabs.vue +16 -14
- package/modules/@apostrophecms/modal/ui/apos/components/AposWidgetModalTabs.vue +16 -14
- package/modules/@apostrophecms/notification/ui/apos/components/AposNotification.vue +93 -91
- package/modules/@apostrophecms/page/index.js +482 -13
- package/modules/@apostrophecms/page/ui/apos/components/AposPagesManager.vue +43 -23
- package/modules/@apostrophecms/page/ui/apos/logic/AposPagesManager.js +248 -156
- package/modules/@apostrophecms/permission/ui/apos/components/AposPermissionGrid.vue +9 -5
- package/modules/@apostrophecms/piece-type/index.js +7 -7
- package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +92 -36
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +30 -24
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapDivider.vue +4 -2
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapLink.vue +2 -1
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapMarks.vue +5 -3
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapStyles.vue +5 -3
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapTable.vue +5 -2
- package/modules/@apostrophecms/schema/index.js +26 -5
- package/modules/@apostrophecms/schema/lib/addFieldTypes.js +42 -9
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputColor.vue +4 -2
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputRange.vue +8 -4
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputRelationship.vue +6 -4
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputSlug.vue +5 -3
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputWrapper.vue +19 -13
- package/modules/@apostrophecms/schema/ui/apos/components/AposSearchList.vue +6 -2
- package/modules/@apostrophecms/schema/ui/apos/components/AposSubform.vue +6 -4
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputSlug.js +28 -25
- package/modules/@apostrophecms/schema/ui/apos/scss/AposInputArray.scss +13 -7
- package/modules/@apostrophecms/settings/ui/apos/components/AposSettingsManager.vue +11 -6
- package/modules/@apostrophecms/translation/ui/apos/components/AposTranslationIndicator.vue +5 -3
- package/modules/@apostrophecms/ui/ui/apos/components/AposAvatar.vue +14 -12
- package/modules/@apostrophecms/ui/ui/apos/components/AposButton.vue +14 -11
- package/modules/@apostrophecms/ui/ui/apos/components/AposButtonSplit.vue +7 -3
- package/modules/@apostrophecms/ui/ui/apos/components/AposCellContextMenu.vue +4 -2
- package/modules/@apostrophecms/ui/ui/apos/components/AposCombo.vue +23 -17
- package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenu.vue +25 -10
- package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenuDialog.vue +7 -5
- package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenuItem.vue +10 -8
- package/modules/@apostrophecms/ui/ui/apos/components/AposEmptyState.vue +9 -5
- package/modules/@apostrophecms/ui/ui/apos/components/AposFile.vue +9 -6
- package/modules/@apostrophecms/ui/ui/apos/components/AposIndicator.vue +5 -0
- package/modules/@apostrophecms/ui/ui/apos/components/AposLoadingBlock.vue +3 -1
- package/modules/@apostrophecms/ui/ui/apos/components/AposLocale.vue +3 -1
- package/modules/@apostrophecms/ui/ui/apos/components/AposLocalePicker.vue +11 -9
- package/modules/@apostrophecms/ui/ui/apos/components/AposMinMaxCount.vue +5 -3
- package/modules/@apostrophecms/ui/ui/apos/components/AposPager.vue +4 -2
- package/modules/@apostrophecms/ui/ui/apos/components/AposPagerDots.vue +8 -6
- package/modules/@apostrophecms/ui/ui/apos/components/AposSlat.vue +25 -17
- package/modules/@apostrophecms/ui/ui/apos/components/AposSlatList.vue +5 -9
- package/modules/@apostrophecms/ui/ui/apos/components/AposSubformPreview.vue +10 -6
- package/modules/@apostrophecms/ui/ui/apos/components/AposTag.vue +9 -7
- package/modules/@apostrophecms/ui/ui/apos/components/AposTagApply.vue +8 -4
- package/modules/@apostrophecms/ui/ui/apos/components/AposTagList.vue +4 -2
- package/modules/@apostrophecms/ui/ui/apos/components/AposTagListItem.vue +7 -5
- package/modules/@apostrophecms/ui/ui/apos/components/AposToggle.vue +16 -0
- package/modules/@apostrophecms/ui/ui/apos/components/AposTree.vue +3 -1
- package/modules/@apostrophecms/ui/ui/apos/components/AposTreeRows.vue +11 -9
- package/modules/@apostrophecms/ui/ui/apos/mixins/AposArchiveMixin.js +2 -2
- package/modules/@apostrophecms/ui/ui/apos/mixins/AposPublishMixin.js +6 -6
- package/modules/@apostrophecms/ui/ui/apos/scss/global/_inputs.scss +30 -22
- package/modules/@apostrophecms/ui/ui/apos/scss/global/_theme.scss +22 -18
- package/modules/@apostrophecms/ui/ui/apos/scss/global/_tooltips.scss +18 -15
- package/modules/@apostrophecms/ui/ui/apos/scss/mixins/_input_mixins.scss +8 -6
- package/modules/@apostrophecms/ui/ui/apos/scss/mixins/_mixins.scss +3 -1
- package/modules/@apostrophecms/ui/ui/apos/scss/mixins/_theme_mixins.scss +34 -19
- package/modules/@apostrophecms/ui/ui/apos/scss/mixins/_type_mixins.scss +3 -1
- package/modules/@apostrophecms/ui/ui/apos/utils/index.js +140 -51
- package/modules/@apostrophecms/widget-type/index.js +3 -2
- package/modules/@apostrophecms/widget-type/ui/apos/mixins/AposWidgetMixin.js +5 -1
- package/package.json +5 -6
- package/test/big-upload.js +111 -0
- package/test/change-doc-ids.js +60 -1
- package/test/pages.js +488 -0
- package/test/schemas.js +327 -0
- package/test/utils.js +266 -5
|
@@ -173,10 +173,12 @@ export default {
|
|
|
173
173
|
.apos-modal__heading {
|
|
174
174
|
@include type-base;
|
|
175
175
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
176
|
+
& {
|
|
177
|
+
display: inline-block;
|
|
178
|
+
margin: 0;
|
|
179
|
+
font-size: var(--a-type-large);
|
|
180
|
+
line-height: $spacing-double;
|
|
181
|
+
}
|
|
180
182
|
}
|
|
181
183
|
|
|
182
184
|
.apos-command-menu-key {
|
|
@@ -205,18 +207,22 @@ export default {
|
|
|
205
207
|
.apos-command-menu-shortcut-group {
|
|
206
208
|
@include type-base;
|
|
207
209
|
|
|
208
|
-
|
|
210
|
+
& {
|
|
211
|
+
font-weight: 400;
|
|
212
|
+
}
|
|
209
213
|
}
|
|
210
214
|
|
|
211
215
|
.apos-command-menu-shortcut-group-title {
|
|
212
216
|
@include type-base;
|
|
213
217
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
218
|
+
& {
|
|
219
|
+
box-sizing: border-box;
|
|
220
|
+
height: 24px;
|
|
221
|
+
margin: 0;
|
|
222
|
+
padding: $spacing-half 0;
|
|
223
|
+
color: var(--a-base-3);
|
|
224
|
+
text-align: left;
|
|
225
|
+
}
|
|
220
226
|
}
|
|
221
227
|
|
|
222
228
|
.apos-command-menu-shortcut-command {
|
|
@@ -334,8 +334,12 @@ module.exports = {
|
|
|
334
334
|
// new document's content wins in the event of a conflict.
|
|
335
335
|
// If `keep` is not set, a `conflict` error is thrown in the
|
|
336
336
|
// event of a conflict.
|
|
337
|
+
//
|
|
338
|
+
// If `skipReplace` is set to `true`, the method will not attempt to remove
|
|
339
|
+
// the old document, but will still update the new document. The new _id
|
|
340
|
+
// for each pair will be used for retrieving the "existing" document in this case.
|
|
337
341
|
|
|
338
|
-
async changeDocIds(pairs, { keep } = {}) {
|
|
342
|
+
async changeDocIds(pairs, { keep, skipReplace = false } = {}) {
|
|
339
343
|
let renamed = 0;
|
|
340
344
|
let kept = 0;
|
|
341
345
|
// Get page paths up front so we can avoid multiple queries when working on path changes
|
|
@@ -347,13 +351,15 @@ module.exports = {
|
|
|
347
351
|
}).toArray();
|
|
348
352
|
for (const pair of pairs) {
|
|
349
353
|
const [ from, to ] = pair;
|
|
350
|
-
const
|
|
354
|
+
const oldAposDocId = from.split(':')[0];
|
|
355
|
+
const existing = await self.apos.doc.db.findOne({ _id: skipReplace ? to : from });
|
|
351
356
|
if (!existing) {
|
|
352
357
|
throw self.apos.error('notfound');
|
|
353
358
|
}
|
|
354
359
|
const replacement = klona(existing);
|
|
355
|
-
|
|
356
|
-
|
|
360
|
+
if (!skipReplace) {
|
|
361
|
+
await self.apos.doc.db.removeOne({ _id: from });
|
|
362
|
+
}
|
|
357
363
|
replacement._id = to;
|
|
358
364
|
const parts = to.split(':');
|
|
359
365
|
replacement.aposDocId = parts[0];
|
|
@@ -366,8 +372,10 @@ module.exports = {
|
|
|
366
372
|
replacement.path = existing.path.replace(existing.aposDocId, replacement.aposDocId);
|
|
367
373
|
}
|
|
368
374
|
try {
|
|
369
|
-
|
|
370
|
-
|
|
375
|
+
if (!skipReplace) {
|
|
376
|
+
await self.apos.doc.db.insertOne(replacement);
|
|
377
|
+
renamed++;
|
|
378
|
+
}
|
|
371
379
|
} catch (e) {
|
|
372
380
|
// First reinsert old doc to prevent content loss on new doc insert failure
|
|
373
381
|
await self.apos.doc.db.insertOne(existing);
|
|
@@ -404,7 +412,7 @@ module.exports = {
|
|
|
404
412
|
throw self.apos.error('conflict');
|
|
405
413
|
}
|
|
406
414
|
}
|
|
407
|
-
if (isPage) {
|
|
415
|
+
if (isPage && !skipReplace) {
|
|
408
416
|
for (const page of pages) {
|
|
409
417
|
if (page.path.includes(oldAposDocId)) {
|
|
410
418
|
await self.apos.doc.db.updateOne({
|
|
@@ -1734,14 +1742,13 @@ module.exports = {
|
|
|
1734
1742
|
}
|
|
1735
1743
|
|
|
1736
1744
|
function forSchema(schema, doc) {
|
|
1745
|
+
if (!doc) {
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1737
1748
|
for (const field of schema) {
|
|
1738
1749
|
if (field.type === 'area' && doc[field.name] && doc[field.name].items) {
|
|
1739
1750
|
for (const widget of doc[field.name].items) {
|
|
1740
|
-
self.walkByMetaType(widget,
|
|
1741
|
-
arrayItem: handlers.arrayItem,
|
|
1742
|
-
object: handlers.object,
|
|
1743
|
-
relationship: handlers.relationship
|
|
1744
|
-
});
|
|
1751
|
+
self.walkByMetaType(widget, handlers);
|
|
1745
1752
|
}
|
|
1746
1753
|
} else if (field.type === 'array') {
|
|
1747
1754
|
if (doc[field.name]) {
|
|
@@ -1752,14 +1759,10 @@ module.exports = {
|
|
|
1752
1759
|
}
|
|
1753
1760
|
} else if (field.type === 'object') {
|
|
1754
1761
|
const value = doc[field.name];
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
forSchema(field.schema, value);
|
|
1758
|
-
}
|
|
1762
|
+
handlers.object(field, value);
|
|
1763
|
+
forSchema(field.schema, value);
|
|
1759
1764
|
} else if (field.type === 'relationship') {
|
|
1760
|
-
|
|
1761
|
-
handlers.relationship(field, doc);
|
|
1762
|
-
}
|
|
1765
|
+
handlers.relationship(field, doc);
|
|
1763
1766
|
}
|
|
1764
1767
|
}
|
|
1765
1768
|
}
|
|
@@ -759,6 +759,7 @@ module.exports = {
|
|
|
759
759
|
|
|
760
760
|
async convert(req, input, doc, options = {
|
|
761
761
|
presentFieldsOnly: false,
|
|
762
|
+
fetchRelationships: true,
|
|
762
763
|
type: null,
|
|
763
764
|
copyingId: null,
|
|
764
765
|
createId: null
|
|
@@ -783,8 +784,10 @@ module.exports = {
|
|
|
783
784
|
...input
|
|
784
785
|
};
|
|
785
786
|
}
|
|
786
|
-
|
|
787
|
-
|
|
787
|
+
const convertOptions = {
|
|
788
|
+
fetchRelationships: options.fetchRelationships !== false
|
|
789
|
+
};
|
|
790
|
+
await self.apos.schema.convert(req, schema, input, doc, convertOptions);
|
|
788
791
|
|
|
789
792
|
if (options.createId) {
|
|
790
793
|
doc.aposDocId = options.createId;
|
|
@@ -2439,6 +2442,7 @@ module.exports = {
|
|
|
2439
2442
|
// except this one (filtering by topic pares down the list of categories and
|
|
2440
2443
|
// vice versa)
|
|
2441
2444
|
const _query = baseQuery.clone();
|
|
2445
|
+
console.log('filter is:', filter);
|
|
2442
2446
|
_query[filter](null);
|
|
2443
2447
|
choices[filter] = await _query.toChoices(filter, { counts: query.get('counts') });
|
|
2444
2448
|
}
|
|
@@ -610,18 +610,15 @@ export default {
|
|
|
610
610
|
async onSaveDraft({ navigate = false } = {}) {
|
|
611
611
|
await this.save({
|
|
612
612
|
andPublish: false,
|
|
613
|
+
draft: true,
|
|
613
614
|
navigate
|
|
614
615
|
});
|
|
615
|
-
await apos.notify('apostrophe:draftSaved', {
|
|
616
|
-
type: 'success',
|
|
617
|
-
dismiss: true,
|
|
618
|
-
icon: 'file-document-icon'
|
|
619
|
-
});
|
|
620
616
|
},
|
|
621
617
|
async save({
|
|
622
618
|
andPublish = false,
|
|
623
619
|
navigate = false,
|
|
624
620
|
andSubmit = false,
|
|
621
|
+
draft = false,
|
|
625
622
|
keepOpen = false
|
|
626
623
|
}) {
|
|
627
624
|
const body = this.getRequestBody({ update: Boolean(this.currentId) });
|
|
@@ -668,6 +665,13 @@ export default {
|
|
|
668
665
|
this.$emit('modal-result', doc);
|
|
669
666
|
this.modal.showModal = false;
|
|
670
667
|
}
|
|
668
|
+
if (draft) {
|
|
669
|
+
await apos.notify('apostrophe:draftSaved', {
|
|
670
|
+
type: 'success',
|
|
671
|
+
dismiss: true,
|
|
672
|
+
icon: 'file-document-icon'
|
|
673
|
+
});
|
|
674
|
+
}
|
|
671
675
|
if (navigate) {
|
|
672
676
|
if (doc._url) {
|
|
673
677
|
window.location = doc._url;
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
identifier="localePickerTrigger"
|
|
10
10
|
:button="button"
|
|
11
11
|
:unpadded="true"
|
|
12
|
+
:center-on-icon="true"
|
|
12
13
|
menu-placement="bottom-end"
|
|
13
14
|
@open="open"
|
|
14
15
|
@close="isOpen = false"
|
|
@@ -163,15 +164,19 @@ async function switchLocale(locale) {
|
|
|
163
164
|
.apos-doc-locales__label {
|
|
164
165
|
@include type-base;
|
|
165
166
|
|
|
166
|
-
|
|
167
|
-
|
|
167
|
+
& {
|
|
168
|
+
margin-right: 0.3rem;
|
|
169
|
+
font-weight: var(--a-weight-bold);
|
|
170
|
+
}
|
|
168
171
|
}
|
|
169
172
|
|
|
170
173
|
.apos-doc-locales__switcher :deep(.apos-button__label) {
|
|
171
174
|
@include type-base;
|
|
172
175
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
+
& {
|
|
177
|
+
color: var(--a-primary);
|
|
178
|
+
font-weight: var(--a-weight-bold);
|
|
179
|
+
letter-spacing: 1px;
|
|
180
|
+
}
|
|
176
181
|
}
|
|
177
182
|
</style>
|
|
@@ -429,6 +429,18 @@ export default {
|
|
|
429
429
|
|
|
430
430
|
},
|
|
431
431
|
async customAction(doc, operation) {
|
|
432
|
+
if (operation.replaces) {
|
|
433
|
+
const confirm = await apos.confirm({
|
|
434
|
+
heading: 'apostrophe:replaceHeadingPrompt',
|
|
435
|
+
description: this.$t('apostrophe:replaceDescPrompt'),
|
|
436
|
+
affirmativeLabel: 'apostrophe:replace',
|
|
437
|
+
icon: false
|
|
438
|
+
});
|
|
439
|
+
if (!confirm) {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
this.$emit('close', doc);
|
|
443
|
+
}
|
|
432
444
|
const props = {
|
|
433
445
|
moduleName: operation.moduleName || this.moduleName,
|
|
434
446
|
// For backwards compatibility
|
|
@@ -3,10 +3,15 @@ const qs = require('qs');
|
|
|
3
3
|
const fetch = require('node-fetch');
|
|
4
4
|
const tough = require('tough-cookie');
|
|
5
5
|
const escapeHost = require('../../../lib/escape-host');
|
|
6
|
+
const util = require('util');
|
|
6
7
|
|
|
7
8
|
module.exports = {
|
|
8
9
|
options: {
|
|
9
|
-
alias: 'http'
|
|
10
|
+
alias: 'http',
|
|
11
|
+
// 2 hour limit to process a "big upload,"
|
|
12
|
+
// which could be something like an entire site
|
|
13
|
+
// with its attachments
|
|
14
|
+
bigUploadMaxSeconds: 2 * 60 * 60
|
|
10
15
|
},
|
|
11
16
|
init(self) {
|
|
12
17
|
// Map friendly errors created via `apos.error` to status codes.
|
|
@@ -27,6 +32,16 @@ module.exports = {
|
|
|
27
32
|
};
|
|
28
33
|
_.merge(self.errors, self.options.addErrors);
|
|
29
34
|
},
|
|
35
|
+
handlers(self) {
|
|
36
|
+
// Wait for the db module to be ready
|
|
37
|
+
return {
|
|
38
|
+
'apostrophe:modulesRegistered': {
|
|
39
|
+
setCollection() {
|
|
40
|
+
self.bigUploads = self.apos.db.collection('aposBigUploads');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
},
|
|
30
45
|
methods(self) {
|
|
31
46
|
return {
|
|
32
47
|
// Add another friendly error name to http status code mapping so you
|
|
@@ -203,7 +218,7 @@ module.exports = {
|
|
|
203
218
|
}
|
|
204
219
|
if (options.body && options.body.constructor && (options.body.constructor.name === 'FormData')) {
|
|
205
220
|
// If we don't do this multiparty will not parse it properly
|
|
206
|
-
const contentLength = await
|
|
221
|
+
const contentLength = await util.promisify((callback) => {
|
|
207
222
|
return options.body.getLength(callback);
|
|
208
223
|
})();
|
|
209
224
|
options.headers = options.headers || {};
|
|
@@ -294,8 +309,9 @@ module.exports = {
|
|
|
294
309
|
getBase() {
|
|
295
310
|
const server = self.apos.modules['@apostrophecms/express'].server;
|
|
296
311
|
return `http://${escapeHost(server.address().address)}:${server.address().port}`;
|
|
297
|
-
}
|
|
312
|
+
},
|
|
298
313
|
|
|
314
|
+
...require('./lib/big-upload-middleware.js')(self)
|
|
299
315
|
};
|
|
300
316
|
}
|
|
301
317
|
};
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
const multiparty = require('connect-multiparty');
|
|
2
|
+
const util = require('util');
|
|
3
|
+
const {
|
|
4
|
+
readFile, open, unlink
|
|
5
|
+
} = require('node:fs/promises');
|
|
6
|
+
|
|
7
|
+
module.exports = (self) => ({
|
|
8
|
+
// Returns middleware that allows any route to receive large
|
|
9
|
+
// uploads made via big-upload-client. A workaround for
|
|
10
|
+
// the max POST size, max uploaded file size, etc. of
|
|
11
|
+
// nginx and other proxy servers.
|
|
12
|
+
//
|
|
13
|
+
// If an `authorize` function is supplied, it will be invoked
|
|
14
|
+
// with `req` at the start of each request. If it throws
|
|
15
|
+
// an error, a 403 forbidden error is sent. Use this mechanism
|
|
16
|
+
// to block unauthorized use and potential denial of service.
|
|
17
|
+
|
|
18
|
+
bigUploadMiddleware({ authorize } = {}) {
|
|
19
|
+
return (req, res, next) => {
|
|
20
|
+
// Chain the multiparty middleware to handle normal uploads
|
|
21
|
+
// as chunks (more efficient than base64 etc)
|
|
22
|
+
const multipartyFn = multiparty();
|
|
23
|
+
return multipartyFn(req, res, () => {
|
|
24
|
+
return body(req, res, next);
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
async function body(req, res, next) {
|
|
29
|
+
const origFiles = req.files;
|
|
30
|
+
try {
|
|
31
|
+
if (authorize) {
|
|
32
|
+
try {
|
|
33
|
+
await authorize(req);
|
|
34
|
+
} catch (e) {
|
|
35
|
+
self.logError('bigUploadUnauthorized', e);
|
|
36
|
+
return res.status(403).send({
|
|
37
|
+
name: 'forbidden',
|
|
38
|
+
message: 'Unauthorized aposBigUpload request'
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const params = req.query.aposBigUpload;
|
|
43
|
+
if (!params) {
|
|
44
|
+
return next();
|
|
45
|
+
}
|
|
46
|
+
if (params.type === 'start') {
|
|
47
|
+
return await self.bigUploadStart(req, req.body.files);
|
|
48
|
+
} else if (params.type === 'chunk') {
|
|
49
|
+
return await self.bigUploadChunk(req, params);
|
|
50
|
+
} else if (params.type === 'end') {
|
|
51
|
+
return await self.bigUploadEnd(req, params.id, next);
|
|
52
|
+
} else {
|
|
53
|
+
return res.status(400).send({
|
|
54
|
+
name: 'invalid',
|
|
55
|
+
message: 'Invalid aposBigUpload request'
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
} finally {
|
|
59
|
+
// Clean up multiparty temporary files
|
|
60
|
+
for (const { path } of Object.values(origFiles || {})) {
|
|
61
|
+
try {
|
|
62
|
+
await unlink(path);
|
|
63
|
+
} catch (e) {
|
|
64
|
+
// OK if it is already gone
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
async bigUploadStart(req, files = {}) {
|
|
72
|
+
await self.bigUploadCleanup();
|
|
73
|
+
try {
|
|
74
|
+
const id = self.apos.util.generateId();
|
|
75
|
+
files = Object.fromEntries(Object.entries(files).map(([ param, info ]) => {
|
|
76
|
+
if ((typeof param) !== 'string') {
|
|
77
|
+
throw invalid('param');
|
|
78
|
+
}
|
|
79
|
+
if (((typeof info) !== 'object') || (info == null)) {
|
|
80
|
+
throw invalid('info');
|
|
81
|
+
}
|
|
82
|
+
if ((typeof info.name) !== 'string') {
|
|
83
|
+
throw invalid('name');
|
|
84
|
+
}
|
|
85
|
+
if (!info.name.length) {
|
|
86
|
+
throw invalid('name empty');
|
|
87
|
+
}
|
|
88
|
+
if ((typeof info.size) !== 'number') {
|
|
89
|
+
throw invalid('size');
|
|
90
|
+
}
|
|
91
|
+
if ((typeof info.chunks) !== 'number') {
|
|
92
|
+
throw invalid('chunks');
|
|
93
|
+
}
|
|
94
|
+
return [ param, {
|
|
95
|
+
name: info.name,
|
|
96
|
+
size: info.size,
|
|
97
|
+
chunks: info.chunks
|
|
98
|
+
} ];
|
|
99
|
+
}));
|
|
100
|
+
await self.bigUploads.insert({
|
|
101
|
+
_id: id,
|
|
102
|
+
files,
|
|
103
|
+
start: Date.now()
|
|
104
|
+
});
|
|
105
|
+
return req.res.send({
|
|
106
|
+
id
|
|
107
|
+
});
|
|
108
|
+
} catch (e) {
|
|
109
|
+
return req.res.status(500).send({
|
|
110
|
+
name: 'error',
|
|
111
|
+
message: 'aposBigUpload error'
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
function invalid(s) {
|
|
115
|
+
self.apos.util.error(`Invalid bigUpload parameter: ${s}`);
|
|
116
|
+
return self.apos.error('invalid', s);
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
async bigUploadChunk(req, params) {
|
|
121
|
+
try {
|
|
122
|
+
const id = self.apos.launder.id(params.id);
|
|
123
|
+
const n = self.apos.launder.integer(params.n);
|
|
124
|
+
const chunk = self.apos.launder.integer(params.chunk);
|
|
125
|
+
const bigUpload = await self.bigUploads.findOne({ _id: id });
|
|
126
|
+
if (!bigUpload) {
|
|
127
|
+
throw self.apos.error('notfound');
|
|
128
|
+
}
|
|
129
|
+
if ((n < 0) || (n >= Object.keys(bigUpload.files).length)) {
|
|
130
|
+
throw self.apos.error('invalid', 'n out of range');
|
|
131
|
+
}
|
|
132
|
+
const info = Object.values(bigUpload.files)[n];
|
|
133
|
+
if ((chunk < 0) || (chunk >= info.chunks)) {
|
|
134
|
+
throw self.apos.error('invalid', 'chunk out of range');
|
|
135
|
+
}
|
|
136
|
+
const file = req.files.chunk;
|
|
137
|
+
const ufs = self.getBigUploadFs();
|
|
138
|
+
const ufsPath = `/big-uploads/${id}-${n}-${chunk}`;
|
|
139
|
+
await ufs.copyIn(file.path, ufsPath);
|
|
140
|
+
return req.res.send({});
|
|
141
|
+
} catch (e) {
|
|
142
|
+
self.logError('bigUploadError', e);
|
|
143
|
+
return req.res.status(500).send({
|
|
144
|
+
name: 'error',
|
|
145
|
+
message: 'aposBigUpload error'
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
async bigUploadEnd(req, id, next) {
|
|
151
|
+
const ufs = self.getBigUploadFs();
|
|
152
|
+
let bigUpload;
|
|
153
|
+
try {
|
|
154
|
+
bigUpload = await self.bigUploads.findOne({
|
|
155
|
+
_id: id
|
|
156
|
+
});
|
|
157
|
+
if (!bigUpload) {
|
|
158
|
+
return req.res.status(400).send({
|
|
159
|
+
name: 'invalid'
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
let n = 0;
|
|
163
|
+
req.files = {};
|
|
164
|
+
for (const [ param, {
|
|
165
|
+
name, chunks
|
|
166
|
+
} ] of Object.entries(bigUpload.files)) {
|
|
167
|
+
let ext = require('path').extname(name);
|
|
168
|
+
if (ext) {
|
|
169
|
+
ext = ext.substring(1);
|
|
170
|
+
} else {
|
|
171
|
+
ext = 'tmp';
|
|
172
|
+
}
|
|
173
|
+
const tmp = `${ufs.getTempPath()}/${id}-${n}.${ext}`;
|
|
174
|
+
const out = await open(tmp, 'w');
|
|
175
|
+
for (let i = 0; (i < chunks); i++) {
|
|
176
|
+
const ufsPath = `/big-uploads/${id}-${n}-${i}`;
|
|
177
|
+
const chunkTmp = `${tmp}.${i}`;
|
|
178
|
+
try {
|
|
179
|
+
await ufs.copyOut(ufsPath, chunkTmp);
|
|
180
|
+
const data = await readFile(chunkTmp);
|
|
181
|
+
await out.writeFile(data);
|
|
182
|
+
} finally {
|
|
183
|
+
try {
|
|
184
|
+
await unlink(chunkTmp);
|
|
185
|
+
} catch (e) {
|
|
186
|
+
// Probably never got that far
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
await out.close();
|
|
191
|
+
n++;
|
|
192
|
+
req.files[param] = {
|
|
193
|
+
name,
|
|
194
|
+
path: tmp
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
return next();
|
|
198
|
+
} catch (e) {
|
|
199
|
+
self.logError('bigUploadError', e);
|
|
200
|
+
return req.res.status(500).send({
|
|
201
|
+
name: 'error',
|
|
202
|
+
message: 'aposBigUpload error'
|
|
203
|
+
});
|
|
204
|
+
} finally {
|
|
205
|
+
// Intentionally in background
|
|
206
|
+
self.bigUploadCleanupOne(bigUpload);
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
async bigUploadCleanup() {
|
|
211
|
+
const old = await self.bigUploads.find({
|
|
212
|
+
start: {
|
|
213
|
+
$lte: Date.now() - self.options.bigUploadMaxSeconds * 1000
|
|
214
|
+
}
|
|
215
|
+
}).toArray();
|
|
216
|
+
for (const bigUpload of old) {
|
|
217
|
+
await self.bigUploadCleanupOne(bigUpload);
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
async bigUploadCleanupOne(bigUpload) {
|
|
222
|
+
const ufs = self.getBigUploadFs();
|
|
223
|
+
const id = bigUpload._id;
|
|
224
|
+
let n = 0;
|
|
225
|
+
for (const { chunks } of Object.values(bigUpload.files)) {
|
|
226
|
+
for (let i = 0; (i < chunks); i++) {
|
|
227
|
+
const ufsPath = `/big-uploads/${id}-${n}-${i}`;
|
|
228
|
+
try {
|
|
229
|
+
await ufs.remove(ufsPath);
|
|
230
|
+
} catch (e) {
|
|
231
|
+
// It's OK if someone else already removed it
|
|
232
|
+
// or it never got there
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
n++;
|
|
236
|
+
}
|
|
237
|
+
await self.bigUploads.deleteOne({
|
|
238
|
+
_id: bigUpload._id
|
|
239
|
+
});
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
getBigUploadFs() {
|
|
243
|
+
const uploadfs = self.apos.attachment.uploadfs;
|
|
244
|
+
return {
|
|
245
|
+
copyIn: util.promisify(uploadfs.copyIn),
|
|
246
|
+
copyOut: util.promisify(uploadfs.copyOut),
|
|
247
|
+
remove: util.promisify(uploadfs.remove),
|
|
248
|
+
getTempPath: uploadfs.getTempPath
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
});
|
|
@@ -169,7 +169,7 @@
|
|
|
169
169
|
"fileTag": "Datei-Tag",
|
|
170
170
|
"fileTags": "Datei-Tags",
|
|
171
171
|
"fileTypeCannotBeCropped": "{{ extension }}-Dateien können nicht zugeschnitten werden.",
|
|
172
|
-
"fileTypeNotAccepted": "Dieser Dateityp wird nicht akzeptiert. Bitte wähle eine Datei mit einer der folgenden Endungen: {{ extensions }}",
|
|
172
|
+
"fileTypeNotAccepted": "Dieser Dateityp {{ extension}} wird nicht akzeptiert. Bitte wähle eine Datei mit einer der folgenden Endungen: {{ extensions }}",
|
|
173
173
|
"files": "Dateien",
|
|
174
174
|
"filter": "Filter",
|
|
175
175
|
"filterByTag": "Nach Tag filtern",
|
|
@@ -58,6 +58,12 @@
|
|
|
58
58
|
"back": "Back",
|
|
59
59
|
"backToHome": "Back to Home",
|
|
60
60
|
"basics": "Basics",
|
|
61
|
+
"batchArchiveProgress": "Archiving {{ type }}...",
|
|
62
|
+
"batchArchiveCompleted": "Archived {{ count }} {{ type }}.",
|
|
63
|
+
"batchPublishProgress": "Publishing {{ type }}...",
|
|
64
|
+
"batchPublishCompleted": "Published {{ count }} {{ type }}.",
|
|
65
|
+
"batchRestoreProgress": "Restoring {{ type }}...",
|
|
66
|
+
"batchRestoreCompleted": "Restored {{ count }} {{ type }}.",
|
|
61
67
|
"browseDocType": "Browse {{ type }}",
|
|
62
68
|
"cancel": "Cancel",
|
|
63
69
|
"cannotChangeSlugPrefix": "cannot change the slug prefix",
|
|
@@ -194,7 +200,7 @@
|
|
|
194
200
|
"fileTag": "File Tag",
|
|
195
201
|
"fileTags": "File Tags",
|
|
196
202
|
"fileTypeCannotBeCropped": "{{ extension }} files cannot be cropped, do not present the cropping UI for this type",
|
|
197
|
-
"fileTypeNotAccepted": "File type was not accepted. Allowed extensions: {{ extensions }}",
|
|
203
|
+
"fileTypeNotAccepted": "File type {{ extension}} was not accepted. Allowed extensions: {{ extensions }}",
|
|
198
204
|
"fileUploaderAttachmentLimitReached": "Attachment limit reached",
|
|
199
205
|
"fileUploaderDropFile": "Drop a file here or",
|
|
200
206
|
"fileUploaderFieldIsDisabled": "Field is disabled",
|
|
@@ -399,6 +405,8 @@
|
|
|
399
405
|
"removeLink": "Remove Link",
|
|
400
406
|
"removeRichTextAnchor": "Remove Rich Text Anchor",
|
|
401
407
|
"replace": "Replace",
|
|
408
|
+
"replaceHeadingPrompt": "Replace Document?",
|
|
409
|
+
"replaceDescPrompt": "This operation will replace the contents of this document. Are you sure?",
|
|
402
410
|
"resolveErrorsBeforeSaving": "Resolve errors before saving.",
|
|
403
411
|
"resolveErrorsFirst": "Resolve errors first.",
|
|
404
412
|
"restore": "Restore",
|
|
@@ -158,7 +158,7 @@
|
|
|
158
158
|
"fileInvalid": "El archivo no fue aceptado. Puede estar corrupto o su contenido no coincide con su extensión de archivo.",
|
|
159
159
|
"fileTag": "Etiqueta de Archivo",
|
|
160
160
|
"fileTags": "Etiquetas del Archivo",
|
|
161
|
-
"fileTypeNotAccepted": "El tipo de archivo no fue acceptado. Extensiones de Archivo permitidas: {{ extensions }}",
|
|
161
|
+
"fileTypeNotAccepted": "El tipo de archivo {{ extension }} no fue acceptado. Extensiones de Archivo permitidas: {{ extensions }}",
|
|
162
162
|
"files": "Archivos",
|
|
163
163
|
"filter": "Filtrar",
|
|
164
164
|
"filterByTag": "Filtrar por Etiqueta",
|
|
@@ -156,7 +156,7 @@
|
|
|
156
156
|
"fileTag": "Tag du fichier",
|
|
157
157
|
"fileTags": "Tags du fichier",
|
|
158
158
|
"fileTypeCannotBeCropped": "Les fichiers {{ extension }} ne peuvent être rognés",
|
|
159
|
-
"fileTypeNotAccepted": "Le type du fichier n'a pas été accepté. Extensions autorisées : {{ extensions }}",
|
|
159
|
+
"fileTypeNotAccepted": "Le type du fichier {{ extension }} n'a pas été accepté. Extensions autorisées : {{ extensions }}",
|
|
160
160
|
"files": "Fichiers",
|
|
161
161
|
"filter": "Filtrer",
|
|
162
162
|
"filterByTag": "Filtrer par tag",
|
|
@@ -181,7 +181,7 @@
|
|
|
181
181
|
"fileTag": "Tag del file",
|
|
182
182
|
"fileTags": "Tag dei file",
|
|
183
183
|
"fileTypeCannotBeCropped": "I file {{ extension }} non possono essere ritagliati, non presentare l'interfaccia di ritaglio per questo tipo",
|
|
184
|
-
"fileTypeNotAccepted": "Il tipo di file non è stato accettato. Estensioni consentite: {{ extensions }}",
|
|
184
|
+
"fileTypeNotAccepted": "Il tipo di file {{ extension }} non è stato accettato. Estensioni consentite: {{ extensions }}",
|
|
185
185
|
"files": "File",
|
|
186
186
|
"filter": "Filtro",
|
|
187
187
|
"filterByTag": "Filtra per tag",
|
|
@@ -157,7 +157,7 @@
|
|
|
157
157
|
"fileInvalid": "O arquivo não foi aceito. Ele pode estar corrompido ou seu conteúdo pode não corresponder à extensão do arquivo.",
|
|
158
158
|
"fileTag": "Tag de arquivo",
|
|
159
159
|
"fileTags": "Tags de arquivo",
|
|
160
|
-
"fileTypeNotAccepted": "O tipo de arquivo não foi aceito. Extensões permitidas: {{ extensions }}",
|
|
160
|
+
"fileTypeNotAccepted": "O tipo de arquivo {{ extension }} não foi aceito. Extensões permitidas: {{ extensions }}",
|
|
161
161
|
"files": "Arquivos",
|
|
162
162
|
"filter": "Filtrar",
|
|
163
163
|
"filterByTag": "Filtrar por Tag",
|
|
@@ -162,7 +162,7 @@
|
|
|
162
162
|
"fileInvalid": "Súbor nebol prijatý. Môže byť poškodený alebo sa jeho obsah nemusí zhodovať s príponou súboru.",
|
|
163
163
|
"fileTag": "Značku súboru",
|
|
164
164
|
"fileTags": "Značky súboru",
|
|
165
|
-
"fileTypeNotAccepted": "Typ súboru nebol prijatý. Povolené rozšírenia: {{ extensions }}",
|
|
165
|
+
"fileTypeNotAccepted": "Typ súboru {{ extension }} nebol prijatý. Povolené rozšírenia: {{ extensions }}",
|
|
166
166
|
"files": "Súbory",
|
|
167
167
|
"filter": "Filtrovať",
|
|
168
168
|
"filterByTag": "Filtrovať podľa značky",
|
|
@@ -369,6 +369,9 @@ module.exports = {
|
|
|
369
369
|
apiRoutes(self) {
|
|
370
370
|
return {
|
|
371
371
|
get: {
|
|
372
|
+
locales(req) {
|
|
373
|
+
return self.locales;
|
|
374
|
+
},
|
|
372
375
|
async localesPermissions(req) {
|
|
373
376
|
const action = self.apos.launder.string(req.query.action);
|
|
374
377
|
const type = self.apos.launder.string(req.query.type);
|