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.
Files changed (124) hide show
  1. package/.github/workflows/main.yml +1 -1
  2. package/CHANGELOG.md +39 -1
  3. package/lib/big-upload-client.js +100 -0
  4. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBar.vue +5 -3
  5. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBarLocale.vue +6 -3
  6. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBarUser.vue +4 -1
  7. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +24 -16
  8. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextTitle.vue +1 -0
  9. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposSavingIndicator.vue +7 -5
  10. package/modules/@apostrophecms/area/index.js +5 -2
  11. package/modules/@apostrophecms/area/ui/apos/components/AposAreaContextualMenu.vue +20 -12
  12. package/modules/@apostrophecms/area/ui/apos/components/AposAreaExpandedMenu.vue +11 -7
  13. package/modules/@apostrophecms/area/ui/apos/components/AposAreaMenu.vue +20 -12
  14. package/modules/@apostrophecms/area/ui/apos/components/AposAreaMenuItem.vue +3 -1
  15. package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +15 -11
  16. package/modules/@apostrophecms/attachment/index.js +4 -2
  17. package/modules/@apostrophecms/command-menu/ui/apos/components/AposCommandMenuKey.vue +28 -24
  18. package/modules/@apostrophecms/command-menu/ui/apos/components/AposCommandMenuShortcut.vue +17 -11
  19. package/modules/@apostrophecms/doc/index.js +22 -19
  20. package/modules/@apostrophecms/doc-type/index.js +6 -2
  21. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +9 -5
  22. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocLocalePicker.vue +10 -5
  23. package/modules/@apostrophecms/doc-type/ui/apos/logic/AposDocContextMenu.js +12 -0
  24. package/modules/@apostrophecms/http/index.js +19 -3
  25. package/modules/@apostrophecms/http/lib/big-upload-middleware.js +251 -0
  26. package/modules/@apostrophecms/i18n/i18n/de.json +1 -1
  27. package/modules/@apostrophecms/i18n/i18n/en.json +9 -1
  28. package/modules/@apostrophecms/i18n/i18n/es.json +1 -1
  29. package/modules/@apostrophecms/i18n/i18n/fr.json +1 -1
  30. package/modules/@apostrophecms/i18n/i18n/it.json +1 -1
  31. package/modules/@apostrophecms/i18n/i18n/pt-BR.json +1 -1
  32. package/modules/@apostrophecms/i18n/i18n/sk.json +1 -1
  33. package/modules/@apostrophecms/i18n/index.js +3 -0
  34. package/modules/@apostrophecms/i18n/ui/apos/components/AposI18nLocalize.vue +30 -16
  35. package/modules/@apostrophecms/i18n/ui/apos/components/AposI18nLocalizeErrors.vue +7 -5
  36. package/modules/@apostrophecms/image/ui/apos/components/AposImageCropper.vue +5 -1
  37. package/modules/@apostrophecms/image/ui/apos/components/AposImageRelationshipEditor.vue +10 -6
  38. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManager.vue +40 -18
  39. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerDisplay.vue +35 -25
  40. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerEditor.vue +11 -5
  41. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerSelections.vue +15 -9
  42. package/modules/@apostrophecms/image/ui/apos/components/AposMediaUploader.vue +39 -31
  43. package/modules/@apostrophecms/job/index.js +1 -1
  44. package/modules/@apostrophecms/login/ui/apos/components/AposLoginForm.vue +9 -7
  45. package/modules/@apostrophecms/login/ui/apos/components/TheAposLogin.vue +17 -13
  46. package/modules/@apostrophecms/login/ui/apos/components/TheAposLoginHeader.vue +30 -20
  47. package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +5 -0
  48. package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +4 -1
  49. package/modules/@apostrophecms/modal/ui/apos/components/AposModalBreadcrumbs.vue +8 -4
  50. package/modules/@apostrophecms/modal/ui/apos/components/AposModalConfirm.vue +14 -8
  51. package/modules/@apostrophecms/modal/ui/apos/components/AposModalShareDraft.vue +32 -22
  52. package/modules/@apostrophecms/modal/ui/apos/components/AposModalTabs.vue +16 -14
  53. package/modules/@apostrophecms/modal/ui/apos/components/AposWidgetModalTabs.vue +16 -14
  54. package/modules/@apostrophecms/notification/ui/apos/components/AposNotification.vue +93 -91
  55. package/modules/@apostrophecms/page/index.js +482 -13
  56. package/modules/@apostrophecms/page/ui/apos/components/AposPagesManager.vue +43 -23
  57. package/modules/@apostrophecms/page/ui/apos/logic/AposPagesManager.js +248 -156
  58. package/modules/@apostrophecms/permission/ui/apos/components/AposPermissionGrid.vue +9 -5
  59. package/modules/@apostrophecms/piece-type/index.js +7 -7
  60. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +92 -36
  61. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +30 -24
  62. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapDivider.vue +4 -2
  63. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapLink.vue +2 -1
  64. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapMarks.vue +5 -3
  65. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapStyles.vue +5 -3
  66. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapTable.vue +5 -2
  67. package/modules/@apostrophecms/schema/index.js +26 -5
  68. package/modules/@apostrophecms/schema/lib/addFieldTypes.js +42 -9
  69. package/modules/@apostrophecms/schema/ui/apos/components/AposInputColor.vue +4 -2
  70. package/modules/@apostrophecms/schema/ui/apos/components/AposInputRange.vue +8 -4
  71. package/modules/@apostrophecms/schema/ui/apos/components/AposInputRelationship.vue +6 -4
  72. package/modules/@apostrophecms/schema/ui/apos/components/AposInputSlug.vue +5 -3
  73. package/modules/@apostrophecms/schema/ui/apos/components/AposInputWrapper.vue +19 -13
  74. package/modules/@apostrophecms/schema/ui/apos/components/AposSearchList.vue +6 -2
  75. package/modules/@apostrophecms/schema/ui/apos/components/AposSubform.vue +6 -4
  76. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputSlug.js +28 -25
  77. package/modules/@apostrophecms/schema/ui/apos/scss/AposInputArray.scss +13 -7
  78. package/modules/@apostrophecms/settings/ui/apos/components/AposSettingsManager.vue +11 -6
  79. package/modules/@apostrophecms/translation/ui/apos/components/AposTranslationIndicator.vue +5 -3
  80. package/modules/@apostrophecms/ui/ui/apos/components/AposAvatar.vue +14 -12
  81. package/modules/@apostrophecms/ui/ui/apos/components/AposButton.vue +14 -11
  82. package/modules/@apostrophecms/ui/ui/apos/components/AposButtonSplit.vue +7 -3
  83. package/modules/@apostrophecms/ui/ui/apos/components/AposCellContextMenu.vue +4 -2
  84. package/modules/@apostrophecms/ui/ui/apos/components/AposCombo.vue +23 -17
  85. package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenu.vue +25 -10
  86. package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenuDialog.vue +7 -5
  87. package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenuItem.vue +10 -8
  88. package/modules/@apostrophecms/ui/ui/apos/components/AposEmptyState.vue +9 -5
  89. package/modules/@apostrophecms/ui/ui/apos/components/AposFile.vue +9 -6
  90. package/modules/@apostrophecms/ui/ui/apos/components/AposIndicator.vue +5 -0
  91. package/modules/@apostrophecms/ui/ui/apos/components/AposLoadingBlock.vue +3 -1
  92. package/modules/@apostrophecms/ui/ui/apos/components/AposLocale.vue +3 -1
  93. package/modules/@apostrophecms/ui/ui/apos/components/AposLocalePicker.vue +11 -9
  94. package/modules/@apostrophecms/ui/ui/apos/components/AposMinMaxCount.vue +5 -3
  95. package/modules/@apostrophecms/ui/ui/apos/components/AposPager.vue +4 -2
  96. package/modules/@apostrophecms/ui/ui/apos/components/AposPagerDots.vue +8 -6
  97. package/modules/@apostrophecms/ui/ui/apos/components/AposSlat.vue +25 -17
  98. package/modules/@apostrophecms/ui/ui/apos/components/AposSlatList.vue +5 -9
  99. package/modules/@apostrophecms/ui/ui/apos/components/AposSubformPreview.vue +10 -6
  100. package/modules/@apostrophecms/ui/ui/apos/components/AposTag.vue +9 -7
  101. package/modules/@apostrophecms/ui/ui/apos/components/AposTagApply.vue +8 -4
  102. package/modules/@apostrophecms/ui/ui/apos/components/AposTagList.vue +4 -2
  103. package/modules/@apostrophecms/ui/ui/apos/components/AposTagListItem.vue +7 -5
  104. package/modules/@apostrophecms/ui/ui/apos/components/AposToggle.vue +16 -0
  105. package/modules/@apostrophecms/ui/ui/apos/components/AposTree.vue +3 -1
  106. package/modules/@apostrophecms/ui/ui/apos/components/AposTreeRows.vue +11 -9
  107. package/modules/@apostrophecms/ui/ui/apos/mixins/AposArchiveMixin.js +2 -2
  108. package/modules/@apostrophecms/ui/ui/apos/mixins/AposPublishMixin.js +6 -6
  109. package/modules/@apostrophecms/ui/ui/apos/scss/global/_inputs.scss +30 -22
  110. package/modules/@apostrophecms/ui/ui/apos/scss/global/_theme.scss +22 -18
  111. package/modules/@apostrophecms/ui/ui/apos/scss/global/_tooltips.scss +18 -15
  112. package/modules/@apostrophecms/ui/ui/apos/scss/mixins/_input_mixins.scss +8 -6
  113. package/modules/@apostrophecms/ui/ui/apos/scss/mixins/_mixins.scss +3 -1
  114. package/modules/@apostrophecms/ui/ui/apos/scss/mixins/_theme_mixins.scss +34 -19
  115. package/modules/@apostrophecms/ui/ui/apos/scss/mixins/_type_mixins.scss +3 -1
  116. package/modules/@apostrophecms/ui/ui/apos/utils/index.js +140 -51
  117. package/modules/@apostrophecms/widget-type/index.js +3 -2
  118. package/modules/@apostrophecms/widget-type/ui/apos/mixins/AposWidgetMixin.js +5 -1
  119. package/package.json +5 -6
  120. package/test/big-upload.js +111 -0
  121. package/test/change-doc-ids.js +60 -1
  122. package/test/pages.js +488 -0
  123. package/test/schemas.js +327 -0
  124. 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
- display: inline-block;
177
- margin: 0;
178
- font-size: var(--a-type-large);
179
- line-height: $spacing-double;
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
- font-weight: 400;
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
- box-sizing: border-box;
215
- height: 24px;
216
- margin: 0;
217
- padding: $spacing-half 0;
218
- color: var(--a-base-3);
219
- text-align: left;
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 existing = await self.apos.doc.db.findOne({ _id: from });
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
- await self.apos.doc.db.removeOne({ _id: from });
356
- const oldAposDocId = existing.aposDocId;
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
- await self.apos.doc.db.insertOne(replacement);
370
- renamed++;
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
- if (value) {
1756
- handlers.object(field, value);
1757
- forSchema(field.schema, value);
1758
- }
1762
+ handlers.object(field, value);
1763
+ forSchema(field.schema, value);
1759
1764
  } else if (field.type === 'relationship') {
1760
- if (Array.isArray(doc[field.name])) {
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
- await self.apos.schema.convert(req, schema, input, doc);
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
- margin-right: 0.3rem;
167
- font-weight: var(--a-weight-bold);
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
- color: var(--a-primary);
174
- font-weight: var(--a-weight-bold);
175
- letter-spacing: 1px;
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 require('util').promisify((callback) => {
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);