apostrophe 3.61.1 → 3.63.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 (104) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBar.vue +1 -1
  3. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +6 -4
  4. package/modules/@apostrophecms/area/ui/apos/components/AposAreaEditor.vue +9 -1
  5. package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +8 -0
  6. package/modules/@apostrophecms/area/ui/apos/components/AposWidgetControls.vue +6 -3
  7. package/modules/@apostrophecms/doc/index.js +256 -7
  8. package/modules/@apostrophecms/doc/ui/apos/mixins/AposFieldMetaUtilsMixin.js +93 -0
  9. package/modules/@apostrophecms/doc-type/index.js +78 -12
  10. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +9 -1
  11. package/modules/@apostrophecms/doc-type/ui/apos/logic/AposDocContextMenu.js +24 -6
  12. package/modules/@apostrophecms/i18n/i18n/en.json +1 -0
  13. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManager.vue +8 -7
  14. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerDisplay.vue +1 -5
  15. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerEditor.vue +5 -2
  16. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerSelections.vue +1 -5
  17. package/modules/@apostrophecms/image/ui/apos/components/AposMediaUploader.vue +4 -2
  18. package/modules/@apostrophecms/login/index.js +25 -19
  19. package/modules/@apostrophecms/login/ui/apos/components/AposLoginForm.vue +11 -1
  20. package/modules/@apostrophecms/login/ui/apos/logic/AposLoginForm.js +46 -2
  21. package/modules/@apostrophecms/modal/ui/apos/components/AposModalShareDraft.vue +8 -3
  22. package/modules/@apostrophecms/modal/ui/apos/mixins/AposEditorMixin.js +3 -0
  23. package/modules/@apostrophecms/page/index.js +118 -27
  24. package/modules/@apostrophecms/page/ui/apos/components/AposPagesManager.vue +3 -1
  25. package/modules/@apostrophecms/page/ui/apos/logic/AposPagesManager.js +7 -0
  26. package/modules/@apostrophecms/page-type/index.js +81 -4
  27. package/modules/@apostrophecms/permission/index.js +60 -31
  28. package/modules/@apostrophecms/piece-type/index.js +19 -41
  29. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +9 -1
  30. package/modules/@apostrophecms/piece-type/ui/apos/components/AposUtilityOperations.vue +16 -1
  31. package/modules/@apostrophecms/rich-text-widget/index.js +141 -1
  32. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +8 -0
  33. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapImage.vue +7 -7
  34. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapLink.vue +38 -79
  35. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Link.js +11 -0
  36. package/modules/@apostrophecms/schema/index.js +9 -0
  37. package/modules/@apostrophecms/schema/lib/addFieldTypes.js +22 -2
  38. package/modules/@apostrophecms/schema/ui/apos/components/AposArrayEditor.vue +1 -0
  39. package/modules/@apostrophecms/schema/ui/apos/components/AposInputArea.vue +4 -1
  40. package/modules/@apostrophecms/schema/ui/apos/components/AposInputArray.vue +7 -8
  41. package/modules/@apostrophecms/schema/ui/apos/components/AposInputObject.vue +2 -0
  42. package/modules/@apostrophecms/schema/ui/apos/components/AposInputSlug.vue +1 -0
  43. package/modules/@apostrophecms/schema/ui/apos/components/AposInputString.vue +1 -0
  44. package/modules/@apostrophecms/schema/ui/apos/components/AposInputWrapper.vue +76 -30
  45. package/modules/@apostrophecms/schema/ui/apos/components/AposSchema.vue +2 -4
  46. package/modules/@apostrophecms/schema/ui/apos/components/AposSearchList.vue +1 -1
  47. package/modules/@apostrophecms/schema/ui/apos/logic/AposArrayEditor.js +7 -0
  48. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputArea.js +13 -1
  49. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputArray.js +5 -1
  50. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputObject.js +21 -0
  51. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputRelationship.js +12 -8
  52. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputWrapper.js +35 -0
  53. package/modules/@apostrophecms/schema/ui/apos/logic/AposSchema.js +6 -0
  54. package/modules/@apostrophecms/schema/ui/apos/mixins/AposInputMixin.js +41 -0
  55. package/modules/@apostrophecms/ui/ui/apos/components/AposCellContextMenu.vue +1 -0
  56. package/modules/@apostrophecms/ui/ui/apos/components/AposCheckbox.vue +1 -0
  57. package/modules/@apostrophecms/ui/ui/apos/components/AposTree.vue +7 -0
  58. package/modules/@apostrophecms/ui/ui/apos/components/AposTreeRows.vue +8 -0
  59. package/modules/@apostrophecms/ui/ui/apos/mixins/AposPublishMixin.js +10 -4
  60. package/modules/@apostrophecms/ui/ui/apos/scss/global/_inputs.scss +2 -2
  61. package/modules/@apostrophecms/widget-type/ui/apos/components/AposWidgetEditor.vue +7 -0
  62. package/modules/@apostrophecms/widget-type/ui/apos/mixins/AposWidgetMixin.js +6 -0
  63. package/package.json +2 -2
  64. package/test/areas.js +1 -1
  65. package/test/assets.js +2 -2
  66. package/test/attachments.js +2 -2
  67. package/test/base-module.js +2 -1
  68. package/test/base-url-env-var.js +2 -2
  69. package/test/change-doc-ids.js +33 -31
  70. package/test/command-menu.js +2 -2
  71. package/test/content-i18n.js +47 -46
  72. package/test/db.js +2 -2
  73. package/test/docs.js +301 -126
  74. package/test/draft-published.js +2 -2
  75. package/test/email.js +2 -1
  76. package/test/express.js +3 -2
  77. package/test/external-front.js +4 -4
  78. package/test/field-meta.js +363 -0
  79. package/test/global.js +2 -1
  80. package/test/http.js +4 -2
  81. package/test/images.js +87 -88
  82. package/test/job.js +34 -34
  83. package/test/locks.js +2 -2
  84. package/test/login-requirements.js +3 -2
  85. package/test/middleware-and-route-order.js +2 -2
  86. package/test/page-type.js +2 -1
  87. package/test/pages-autocomplete.js +192 -0
  88. package/test/pages-public-api.js +2 -2
  89. package/test/pages-rest.js +4 -4
  90. package/test/pages.js +389 -57
  91. package/test/parked-pages.js +47 -47
  92. package/test/permissions.js +76 -0
  93. package/test/pieces-page-type.js +2 -1
  94. package/test/pieces-public-api.js +38 -38
  95. package/test/pieces.js +4 -4
  96. package/test/published-pages.js +16 -16
  97. package/test/rich-text-widget.js +164 -0
  98. package/test/schema-queryBuilders.js +180 -0
  99. package/test/schemaBuilders.js +4 -4
  100. package/test/schemas.js +220 -221
  101. package/test/search.js +2 -2
  102. package/test/soft-redirects.js +2 -1
  103. package/test/templates.js +2 -2
  104. package/test/users.js +2 -1
@@ -19,7 +19,9 @@ module.exports = {
19
19
  relationshipSuggestionIcon: 'text-box-icon',
20
20
  relationshipSuggestionFields: [ 'slug' ]
21
21
  },
22
- cascades: [ 'fields' ],
22
+ // Adding permissions for advanced permissions to allow modules to use it without
23
+ // being forced to check if the module is used with advanced permissions or not.
24
+ cascades: [ 'fields', 'permissions' ],
23
25
  fields(self) {
24
26
  return {
25
27
  add: {
@@ -117,7 +119,7 @@ module.exports = {
117
119
  type: 'command-menu-manager-create-new'
118
120
  },
119
121
  permission: {
120
- action: 'edit',
122
+ action: 'create',
121
123
  type: self.__meta.name
122
124
  },
123
125
  shortcut: 'C'
@@ -145,7 +147,7 @@ module.exports = {
145
147
  type: 'command-menu-manager-archive-selected'
146
148
  },
147
149
  permission: {
148
- action: 'edit',
150
+ action: 'delete',
149
151
  type: self.__meta.name
150
152
  },
151
153
  shortcut: 'E'
@@ -326,6 +328,15 @@ module.exports = {
326
328
  }
327
329
  }
328
330
  },
331
+ afterDelete: {
332
+ async deleteRelatedReverseId(req, doc) {
333
+ // When deleting an unlocalized or draft document,
334
+ // we remove related reverse IDs of documents having a relation to the deleted one
335
+ if (!doc.aposMode || doc.aposMode === 'draft') {
336
+ await self.deleteRelatedReverseId(doc, true);
337
+ }
338
+ }
339
+ },
329
340
  afterRescue: {
330
341
  async revertDeduplication(req, doc) {
331
342
  const $set = await self.getRevertDeduplicationSet(req, doc);
@@ -369,18 +380,27 @@ module.exports = {
369
380
 
370
381
  methods(self) {
371
382
  return {
372
- async updateCacheField(req, doc) {
373
- const relatedDocsIds = self.getRelatedDocsIds(req, doc);
374
-
375
- // - Remove current doc reference from docs that include it
376
- // - Update these docs' cache field
377
- await self.apos.doc.db.updateMany({
383
+ async deleteRelatedReverseId(doc, deleting = false) {
384
+ const locales = doc.aposLocale && deleting
385
+ ? [
386
+ doc.aposLocale.replace(':draft', ':published'),
387
+ doc.aposLocale.replace(':published', ':draft')
388
+ ]
389
+ : [ doc.aposLocale ];
390
+ return self.apos.doc.db.updateMany({
378
391
  relatedReverseIds: { $in: [ doc.aposDocId ] },
379
- aposLocale: { $in: [ doc.aposLocale, null ] }
392
+ aposLocale: { $in: [ ...locales, null ] }
380
393
  }, {
381
394
  $pull: { relatedReverseIds: doc.aposDocId },
382
395
  $set: { cacheInvalidatedAt: doc.updatedAt }
383
396
  });
397
+ },
398
+ async updateCacheField(req, doc) {
399
+ const relatedDocsIds = self.getRelatedDocsIds(req, doc);
400
+
401
+ // - Remove current doc reference from docs that include it
402
+ // - Update these docs' cache field
403
+ await this.deleteRelatedReverseId(doc);
384
404
 
385
405
  if (relatedDocsIds.length) {
386
406
  // - Add current doc reference to related docs
@@ -421,7 +441,8 @@ module.exports = {
421
441
  label: 'apostrophe:shareDraft',
422
442
  modal: 'AposModalShareDraft',
423
443
  manuallyPublished: true,
424
- hasUrl: true
444
+ hasUrl: true,
445
+ conditions: [ 'canShareDraft' ]
425
446
  });
426
447
  },
427
448
  getRelatedDocsIds(req, doc) {
@@ -1476,11 +1497,52 @@ module.exports = {
1476
1497
  return field.viewPermission && !self.apos.permission.can(req, field.viewPermission.action, field.viewPermission.type);
1477
1498
  });
1478
1499
 
1500
+ if (doc.aposMeta && !self.apos.permission.can(req, 'edit', doc)) {
1501
+ forbiddenSchemaFields.push({
1502
+ name: 'aposMeta'
1503
+ });
1504
+ }
1505
+
1479
1506
  forbiddenSchemaFields.forEach(field => {
1480
1507
  delete doc[field.name];
1481
1508
  });
1482
1509
 
1483
1510
  return doc;
1511
+ },
1512
+
1513
+ composeFilters() {
1514
+ self.filters = Object.keys(self.filters).map((key) => ({
1515
+ name: key,
1516
+ ...self.filters[key],
1517
+ inputType: self.filters[key].inputType || 'select'
1518
+ }));
1519
+ // Add a null choice if not already added or set to `required`
1520
+ self.filters.forEach((filter) => {
1521
+ if (filter.choices) {
1522
+ if (
1523
+ !filter.required &&
1524
+ filter.choices &&
1525
+ !filter.choices.find((choice) => choice.value === null)
1526
+ ) {
1527
+ filter.def = null;
1528
+ filter.choices.push({
1529
+ value: null,
1530
+ label: 'apostrophe:none'
1531
+ });
1532
+ }
1533
+ } else {
1534
+ // Dynamic choices from the REST API, but
1535
+ // we need a label for "no opinion"
1536
+ filter.nullLabel = 'Choose One';
1537
+ }
1538
+ });
1539
+ },
1540
+
1541
+ composeColumns() {
1542
+ self.columns = Object.keys(self.columns).map((key) => ({
1543
+ name: key,
1544
+ ...self.columns[key]
1545
+ }));
1484
1546
  }
1485
1547
  };
1486
1548
  },
@@ -1499,8 +1561,10 @@ module.exports = {
1499
1561
  label,
1500
1562
  pluralLabel,
1501
1563
  relatedDocument: self.options.relatedDocument,
1564
+ canCreate: self.apos.permission.can(req, 'create', self.name, 'draft'),
1502
1565
  canEdit: self.apos.permission.can(req, 'edit', self.name, 'draft'),
1503
- canPublish: self.apos.permission.can(req, 'publish', self.name)
1566
+ canPublish: self.apos.permission.can(req, 'publish', self.name),
1567
+ canArchive: self.apos.permission.can(req, 'delete', self.name)
1504
1568
  };
1505
1569
  browserOptions.canLocalize = browserOptions.canEdit &&
1506
1570
  self.options.localized &&
@@ -1868,8 +1932,10 @@ module.exports = {
1868
1932
  after(results) {
1869
1933
  // In all cases we mark the docs with ._edit and ._publish if
1870
1934
  // the req is permitted to do those things
1935
+ self.apos.permission.annotate(query.req, 'create', results);
1871
1936
  self.apos.permission.annotate(query.req, 'edit', results);
1872
1937
  self.apos.permission.annotate(query.req, 'publish', results);
1938
+ self.apos.permission.annotate(query.req, 'delete', results);
1873
1939
  }
1874
1940
  },
1875
1941
 
@@ -19,6 +19,7 @@
19
19
  :current="docFields.data"
20
20
  :published="published"
21
21
  :show-edit="false"
22
+ :can-delete-draft="moduleOptions.canDeleteDraft"
22
23
  @close="close"
23
24
  />
24
25
  <AposButton
@@ -67,6 +68,7 @@
67
68
  :conditional-fields="conditionalFields"
68
69
  :doc-id="docId"
69
70
  :value="docFields"
71
+ :meta="docMeta"
70
72
  :server-errors="serverErrors"
71
73
  :ref="tab.name"
72
74
  :generation="generation"
@@ -92,6 +94,7 @@
92
94
  :conditional-fields="conditionalFields"
93
95
  :doc-id="docId"
94
96
  :value="docFields"
97
+ :meta="docMeta"
95
98
  @input="updateDocFields"
96
99
  @validate="triggerValidate"
97
100
  :modifiers="['small', 'inverted']"
@@ -195,8 +198,13 @@ export default {
195
198
 
196
199
  return this.moduleOptions.canPublish;
197
200
  },
201
+ canCreate() {
202
+ return this.original &&
203
+ !this.original._id &&
204
+ this.moduleOptions.canCreate;
205
+ },
198
206
  saveDisabled() {
199
- if (!this.canEdit) {
207
+ if (!this.canCreate && !this.canEdit) {
200
208
  return true;
201
209
  }
202
210
  if (this.restoreOnly) {
@@ -48,6 +48,12 @@ export default {
48
48
  return true;
49
49
  }
50
50
  },
51
+ canDeleteDraft: {
52
+ type: Boolean,
53
+ default() {
54
+ return true;
55
+ }
56
+ },
51
57
  showCopy: {
52
58
  type: Boolean,
53
59
  default() {
@@ -105,7 +111,7 @@ export default {
105
111
  action: 'edit'
106
112
  }
107
113
  ] : []),
108
- ...((this.showPreview && this.hasUrl) ? [
114
+ ...((this.showPreview && this.canPreview) ? [
109
115
  {
110
116
  label: 'apostrophe:preview',
111
117
  action: 'preview'
@@ -245,7 +251,10 @@ export default {
245
251
  if (!this.context._id) {
246
252
  return false;
247
253
  }
248
- if (!this.canEdit) {
254
+ if (!this.context.lastPublishedAt && !this.canDeleteDraft && !this.context._delete) {
255
+ return false;
256
+ }
257
+ if (this.context.lastPublishedAt && (!this.context.modified || !this.context._edit)) {
249
258
  return false;
250
259
  }
251
260
  return (
@@ -262,12 +271,12 @@ export default {
262
271
  },
263
272
  canArchive() {
264
273
  return (
265
- this.canEdit &&
274
+ this.context._delete &&
266
275
  this.context._id &&
267
276
  !this.moduleOptions.singleton &&
268
277
  !this.context.archived &&
269
278
  !this.context.parked &&
270
- ((this.moduleOptions.canPublish && this.context.lastPublishedAt) || !this.manuallyPublished)
279
+ (Boolean(this.canPublish && this.context.lastPublishedAt) || !this.manuallyPublished)
271
280
  );
272
281
  },
273
282
  canUnpublish() {
@@ -280,7 +289,8 @@ export default {
280
289
  );
281
290
  },
282
291
  canCopy() {
283
- return this.canEdit &&
292
+ return this.moduleOptions.canCreate &&
293
+ this.canEdit &&
284
294
  this.moduleOptions.canEdit &&
285
295
  !this.moduleOptions.singleton &&
286
296
  this.context._id;
@@ -290,9 +300,17 @@ export default {
290
300
  this.canEdit &&
291
301
  this.context._id &&
292
302
  this.context.archived &&
293
- ((this.moduleOptions.canPublish && this.context.lastPublishedAt) || !this.manuallyPublished)
303
+ ((!this.manuallyPublished && this.canPublish) || this.manuallyPublished)
294
304
  );
295
305
  },
306
+ canPreview() {
307
+ return this.hasUrl &&
308
+ !this.context.archived;
309
+ },
310
+ canShareDraft() {
311
+ return this.canEdit &&
312
+ !this.context.archived;
313
+ },
296
314
  manuallyPublished() {
297
315
  return this.moduleOptions.localized && !this.autopublish;
298
316
  },
@@ -165,6 +165,7 @@
165
165
  "errorWhilePublishing": "An Error Occurred While Publishing",
166
166
  "errorWhilePublishingDocument": "An error occurred while publishing the document.",
167
167
  "errorWhilePublishingParentPage": "An error occurred while publishing a parent page.",
168
+ "errorWhilePublishingParentPageForbidden": "Because you do not have permission to publish the parent page, we are saving and submitting your draft. Please let your manager know that the parent page needs to be published.",
168
169
  "errorWhileRestoring": "An error occurred while restoring the previously published version.",
169
170
  "errorWhileRestoringArchive": "An error occurred while restoring the document from the archive.",
170
171
  "errorWhileRestoringPrevious": "An error occurred while restoring the previously published version.",
@@ -71,7 +71,6 @@
71
71
  :accept="accept"
72
72
  :items="items"
73
73
  :module-options="moduleOptions"
74
- :can-edit="moduleOptions.canEdit"
75
74
  @edit="updateEditing"
76
75
  v-model="checked"
77
76
  @select="select"
@@ -106,7 +105,6 @@
106
105
  />
107
106
  <AposMediaManagerSelections
108
107
  :items="selected"
109
- :can-edit="moduleOptions.canEdit"
110
108
  @clear="clearSelected" @edit="updateEditing"
111
109
  v-show="!editing"
112
110
  />
@@ -248,7 +246,8 @@ export default {
248
246
  async getMedia (options) {
249
247
  const qs = {
250
248
  ...this.filterValues,
251
- page: this.currentPage
249
+ page: this.currentPage,
250
+ viewContext: this.relationshipField ? 'relationship' : 'manage'
252
251
  };
253
252
  const filtered = !!Object.keys(this.filterValues).length;
254
253
  if (this.moduleOptions && Array.isArray(this.moduleOptions.filters)) {
@@ -354,9 +353,7 @@ export default {
354
353
  this.editing = undefined;
355
354
  },
356
355
  async updateEditing(id) {
357
- if (!this.moduleOptions.canEdit) {
358
- return;
359
- }
356
+ const item = this.items.find(item => item._id === id);
360
357
  // We only care about the current doc for this prompt,
361
358
  // we are not in danger of discarding a selection when
362
359
  // we switch images
@@ -371,7 +368,11 @@ export default {
371
368
  return false;
372
369
  }
373
370
  }
374
- this.editing = this.items.find(item => item._id === id);
371
+ if (!item?._edit) {
372
+ this.editing = null;
373
+ return true;
374
+ }
375
+ this.editing = item;
375
376
  return true;
376
377
  },
377
378
  // select setters
@@ -2,7 +2,7 @@
2
2
  <div class="apos-media-manager-display">
3
3
  <div class="apos-media-manager-display__grid">
4
4
  <AposMediaUploader
5
- v-if="canEdit"
5
+ v-if="moduleOptions.canCreate"
6
6
  :disabled="maxReached"
7
7
  :action="moduleOptions.action"
8
8
  :accept="accept"
@@ -83,10 +83,6 @@ export default {
83
83
  event: 'change'
84
84
  },
85
85
  props: {
86
- canEdit: {
87
- type: Boolean,
88
- default: false
89
- },
90
86
  maxReached: {
91
87
  type: Boolean,
92
88
  default: false
@@ -171,7 +171,7 @@ export default {
171
171
  action: 'localize'
172
172
  });
173
173
  }
174
- if (this.activeMedia._id && !this.restoreOnly) {
174
+ if (this.activeMedia._id && this.activeMedia._delete && !this.restoreOnly) {
175
175
  menu.push({
176
176
  label: 'apostrophe:archiveImage',
177
177
  action: 'archive',
@@ -209,7 +209,9 @@ export default {
209
209
  return dayjs(this.activeMedia.attachment.createdAt).format(this.$t('apostrophe:dayjsMediaCreatedDateFormat'));
210
210
  },
211
211
  isArchived() {
212
- return this.media.archived;
212
+ // ?. necessary to avoid reference to null due to
213
+ // race condition when toggling selection off
214
+ return this.media?.archived;
213
215
  }
214
216
  },
215
217
  watch: {
@@ -248,6 +250,7 @@ export default {
248
250
  this[action]();
249
251
  },
250
252
  async updateActiveDoc(newMedia) {
253
+ newMedia = newMedia || {};
251
254
  this.showReplace = false;
252
255
  this.activeMedia = klona(newMedia);
253
256
  this.restoreOnly = !!this.activeMedia.archived;
@@ -30,7 +30,7 @@
30
30
  {{ item.title }}
31
31
  </div>
32
32
  <AposButton
33
- v-if="canEdit"
33
+ v-if="item._edit"
34
34
  label="apostrophe:edit"
35
35
  type="quiet"
36
36
  :modifiers="['no-motion']"
@@ -49,10 +49,6 @@
49
49
  <script>
50
50
  export default {
51
51
  props: {
52
- canEdit: {
53
- type: Boolean,
54
- default: false
55
- },
56
52
  items: {
57
53
  type: Array,
58
54
  required: true
@@ -141,9 +141,11 @@ export default {
141
141
  for (const file of files) {
142
142
  try {
143
143
  const img = await this.insertImage(file, emptyDoc);
144
- imageIds.push(img._id);
144
+ if (img?._id) {
145
+ imageIds.push(img._id);
146
+ }
145
147
  } catch (e) {
146
- const msg = e.body && e.body.message ? e.body.message : this.$t('Upload error');
148
+ const msg = e.body && e.body.message ? e.body.message : this.$t('apostrophe:uploadError');
147
149
  await apos.notify(msg, {
148
150
  type: 'danger',
149
151
  icon: 'alert-circle-icon',
@@ -713,6 +713,18 @@ module.exports = {
713
713
  };
714
714
  },
715
715
 
716
+ async verifyRequirements(req, requirements) {
717
+ for (const [ name, requirement ] of Object.entries(requirements)) {
718
+ try {
719
+ await requirement.verify(req, req.body.requirements && req.body.requirements[name]);
720
+ } catch (e) {
721
+ e.data = e.data || {};
722
+ e.data.requirement = name;
723
+ throw e;
724
+ }
725
+ }
726
+ },
727
+
716
728
  // Implementation detail of the login route. Log in the user, or if there are
717
729
  // `requirements` that require password verification occur first, return an incomplete token.
718
730
  async initialLogin(req) {
@@ -734,16 +746,14 @@ module.exports = {
734
746
 
735
747
  try {
736
748
  // Initial login step
737
- const { earlyRequirements, lateRequirements } = self.filterRequirements();
738
- for (const [ name, requirement ] of Object.entries(earlyRequirements)) {
739
- try {
740
- await requirement.verify(req, req.body.requirements && req.body.requirements[name]);
741
- } catch (e) {
742
- e.data = e.data || {};
743
- e.data.requirement = name;
744
- throw e;
745
- }
746
- }
749
+ const {
750
+ earlyRequirements,
751
+ onTimeRequirements,
752
+ lateRequirements
753
+ } = self.filterRequirements();
754
+ await self.verifyRequirements(req, earlyRequirements);
755
+ await self.verifyRequirements(req, onTimeRequirements);
756
+
747
757
  // send log information
748
758
  const user = await self.apos.login.verifyLogin(
749
759
  username,
@@ -758,7 +768,6 @@ module.exports = {
758
768
  }
759
769
 
760
770
  const requirementsToVerify = Object.keys(lateRequirements);
761
-
762
771
  if (requirementsToVerify.length) {
763
772
  const token = cuid();
764
773
 
@@ -813,15 +822,12 @@ module.exports = {
813
822
  },
814
823
 
815
824
  filterRequirements() {
825
+ const requirements = Object.entries(self.requirements);
826
+
816
827
  return {
817
- earlyRequirements: Object.fromEntries(
818
- Object.entries(self.requirements)
819
- .filter(([ _, requirement ]) => requirement.phase === 'beforeSubmit')
820
- ),
821
- lateRequirements: Object.fromEntries(
822
- Object.entries(self.requirements)
823
- .filter(([ _, requirement ]) => requirement.phase === 'afterPasswordVerified')
824
- )
828
+ earlyRequirements: Object.fromEntries(requirements.filter(([ , requirement ]) => requirement.phase === 'beforeSubmit')),
829
+ onTimeRequirements: Object.fromEntries(requirements.filter(requirement => requirement.phase === 'uponSubmit')),
830
+ lateRequirements: Object.fromEntries(requirements.filter(([ , requirement ]) => requirement.phase === 'afterPasswordVerified'))
825
831
  };
826
832
  },
827
833
 
@@ -2,7 +2,7 @@
2
2
  <div
3
3
  key="1"
4
4
  class="apos-login-form"
5
- v-if="phase === 'beforeSubmit'"
5
+ v-if="phase === 'beforeSubmit' || phase === 'uponSubmit'"
6
6
  >
7
7
  <TheAposLoginHeader
8
8
  :env="context.env"
@@ -30,6 +30,16 @@
30
30
  @done="requirementDone(requirement, $event)"
31
31
  @block="requirementBlock(requirement)"
32
32
  />
33
+ <template v-if="phase === 'uponSubmit'">
34
+ <Component
35
+ v-for="requirement in uponSubmitRequirements"
36
+ :key="requirement.name"
37
+ :is="requirement.component"
38
+ v-bind="getRequirementProps(requirement.name)"
39
+ @done="requirementDone(requirement, $event)"
40
+ @block="requirementBlock(requirement)"
41
+ />
42
+ </template>
33
43
  <AposButton
34
44
  data-apos-test="loginSubmit"
35
45
  :busy="busy"
@@ -41,6 +41,9 @@ export default {
41
41
  beforeSubmitRequirements() {
42
42
  return this.requirements.filter(requirement => requirement.phase === 'beforeSubmit');
43
43
  },
44
+ uponSubmitRequirements() {
45
+ return this.requirements.filter(requirement => requirement.phase === 'uponSubmit');
46
+ },
44
47
  // The currently active requirement expecting a solo presentation.
45
48
  // Currently it only concerns `afterPasswordVerified` requirements.
46
49
  // beforeSubmit requirements are not presented solo.
@@ -83,6 +86,37 @@ export default {
83
86
  } else {
84
87
  return null;
85
88
  }
89
+ },
90
+ uponSubmitRequirements: {
91
+ deep: true,
92
+ async handler(newVal) {
93
+ if (this.phase !== 'uponSubmit') {
94
+ return;
95
+ }
96
+
97
+ const isUponSubmitRequirementsPending = newVal.some(requirement => requirement.done === null);
98
+ if (isUponSubmitRequirementsPending) {
99
+ return;
100
+ }
101
+
102
+ const isUponSubmitRequirementsDone = newVal.every(requirement => requirement.done === true) || this.uponSubmitRequirements.length === 0;
103
+ if (isUponSubmitRequirementsDone) {
104
+ await this.postSubmit();
105
+
106
+ return;
107
+ }
108
+
109
+ const isUponSubmitRequirementsBlocked = newVal.some(requirement => requirement.done === false);
110
+ if (isUponSubmitRequirementsBlocked) {
111
+ for (const requirement of this.uponSubmitRequirements) {
112
+ requirement.done = null;
113
+ requirement.value = null;
114
+ }
115
+ this.phase = 'beforeSubmit';
116
+ this.busy = false;
117
+ this.error = '';
118
+ }
119
+ }
86
120
  }
87
121
  },
88
122
  created() {
@@ -96,6 +130,15 @@ export default {
96
130
  this.busy = true;
97
131
  this.error = '';
98
132
 
133
+ this.uponSubmitRequirements.length
134
+ ? this.uponSubmit()
135
+ : await this.postSubmit();
136
+ },
137
+ uponSubmit() {
138
+ this.phase = 'uponSubmit';
139
+ // Note: uponSubmitRequirements watcher will handle the next step
140
+ },
141
+ async postSubmit() {
99
142
  await this.invokeInitialLoginApi();
100
143
  },
101
144
  async invokeInitialLoginApi() {
@@ -169,7 +212,7 @@ export default {
169
212
  const requirement = this.requirements
170
213
  .find(requirement => requirement.name === requirementDone.name);
171
214
 
172
- if (requirement.phase === 'beforeSubmit') {
215
+ if (requirement.phase === 'beforeSubmit' || requirement.phase === 'uponSubmit') {
173
216
  requirement.done = true;
174
217
  requirement.value = value;
175
218
  return;
@@ -226,7 +269,7 @@ function getRequirements() {
226
269
  name,
227
270
  component: requirement.component || name,
228
271
  ...requirement,
229
- done: false,
272
+ done: null,
230
273
  value: null,
231
274
  success: null,
232
275
  error: null
@@ -234,6 +277,7 @@ function getRequirements() {
234
277
  });
235
278
  return [
236
279
  ...requirements.filter(r => r.phase === 'beforeSubmit'),
280
+ ...requirements.filter(r => r.phase === 'uponSubmit'),
237
281
  ...requirements.filter(r => r.phase === 'afterPasswordVerified')
238
282
  ];
239
283
  }
@@ -30,7 +30,7 @@
30
30
  class="apos-share-draft__toggle"
31
31
  @toggle="toggle"
32
32
  />
33
- <p class="apos-share-draft__toggle-label">
33
+ <p @click="toggle" class="apos-share-draft__toggle-label">
34
34
  {{ $t('apostrophe:shareDraftEnable') }}
35
35
  </p>
36
36
  </div>
@@ -266,6 +266,7 @@ export default {
266
266
  .apos-share-draft__toggle-wrapper {
267
267
  display: flex;
268
268
  align-items: center;
269
+ margin-bottom: $spacing-base;
269
270
  }
270
271
 
271
272
  .apos-share-draft__toggle {
@@ -273,14 +274,18 @@ export default {
273
274
  }
274
275
 
275
276
  .apos-share-draft__toggle-label {
276
- @include type-base;
277
+ @include type-large;
278
+ flex-grow: 1;
277
279
  max-width: 370px;
278
280
  line-height: var(--a-line-tallest);
279
281
  margin: 0;
282
+ &:hover {
283
+ cursor: pointer;
284
+ }
280
285
  }
281
286
 
282
287
  .apos-share-draft__description {
283
- @include type-small;
288
+ @include type-base;
284
289
  line-height: var(--a-line-tall);
285
290
  max-width: 355px;
286
291
  color: var(--a-base-2);
@@ -46,6 +46,9 @@ export default {
46
46
  // Archive UI is handled via action buttons
47
47
  schema = schema.filter(field => field.name !== 'archived');
48
48
  return schema;
49
+ },
50
+ docMeta() {
51
+ return this.docFields.data?.aposMeta || {};
49
52
  }
50
53
  },
51
54