apostrophe 3.47.0 → 3.49.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 (61) hide show
  1. package/CHANGELOG.md +73 -2
  2. package/index.js +20 -2
  3. package/lib/locales.js +1 -1
  4. package/lib/moog-require.js +7 -0
  5. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +12 -2
  6. package/modules/@apostrophecms/any-page-type/index.js +5 -0
  7. package/modules/@apostrophecms/area/ui/apos/components/AposAreaEditor.vue +2 -0
  8. package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +7 -24
  9. package/modules/@apostrophecms/asset/index.js +27 -2
  10. package/modules/@apostrophecms/asset/lib/webpack/apos/webpack.config.js +23 -2
  11. package/modules/@apostrophecms/asset/lib/webpack/src/webpack.config.js +26 -2
  12. package/modules/@apostrophecms/doc/index.js +149 -0
  13. package/modules/@apostrophecms/doc-type/index.js +40 -1
  14. package/modules/@apostrophecms/global/index.js +4 -15
  15. package/modules/@apostrophecms/i18n/i18n/en.json +3 -2
  16. package/modules/@apostrophecms/i18n/index.js +76 -61
  17. package/modules/@apostrophecms/image/index.js +8 -0
  18. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerDisplay.vue +14 -1
  19. package/modules/@apostrophecms/login/ui/apos/components/AposForgotPasswordForm.vue +3 -60
  20. package/modules/@apostrophecms/login/ui/apos/components/AposLoginForm.vue +3 -231
  21. package/modules/@apostrophecms/login/ui/apos/components/AposResetPasswordForm.vue +3 -96
  22. package/modules/@apostrophecms/login/ui/apos/components/TheAposLogin.vue +2 -99
  23. package/modules/@apostrophecms/login/ui/apos/logic/AposForgotPasswordForm.js +68 -0
  24. package/modules/@apostrophecms/login/ui/apos/logic/AposLoginForm.js +239 -0
  25. package/modules/@apostrophecms/login/ui/apos/logic/AposResetPasswordForm.js +105 -0
  26. package/modules/@apostrophecms/login/ui/apos/logic/TheAposLogin.js +107 -0
  27. package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +9 -3
  28. package/modules/@apostrophecms/modal/ui/apos/components/AposModalToolbar.vue +1 -0
  29. package/modules/@apostrophecms/page/index.js +63 -1
  30. package/modules/@apostrophecms/page-type/index.js +6 -0
  31. package/modules/@apostrophecms/piece-type/index.js +93 -9
  32. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +4 -0
  33. package/modules/@apostrophecms/rich-text-widget/index.js +1 -1
  34. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposImageControlDialog.vue +14 -10
  35. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +252 -86
  36. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapAnchor.vue +0 -1
  37. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapLink.vue +0 -1
  38. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Image.js +76 -54
  39. package/modules/@apostrophecms/schema/index.js +1 -2
  40. package/modules/@apostrophecms/schema/lib/addFieldTypes.js +35 -7
  41. package/modules/@apostrophecms/schema/ui/apos/components/AposArrayEditor.vue +0 -1
  42. package/modules/@apostrophecms/schema/ui/apos/components/AposInputArray.vue +0 -1
  43. package/modules/@apostrophecms/schema/ui/apos/components/AposInputObject.vue +0 -1
  44. package/modules/@apostrophecms/schema/ui/apos/components/AposInputSlug.vue +21 -1
  45. package/modules/@apostrophecms/schema/ui/apos/components/AposInputString.vue +12 -7
  46. package/modules/@apostrophecms/schema/ui/apos/components/AposSchema.vue +7 -1
  47. package/modules/@apostrophecms/ui/ui/apos/components/AposCombo.vue +178 -20
  48. package/modules/@apostrophecms/ui/ui/apos/components/AposFilterMenu.vue +1 -1
  49. package/modules/@apostrophecms/ui/ui/apos/components/AposPager.vue +4 -6
  50. package/modules/@apostrophecms/ui/ui/apos/scss/mixins/_theme_mixins.scss +1 -0
  51. package/modules/@apostrophecms/util/index.js +5 -6
  52. package/modules/@apostrophecms/util/ui/src/http.js +6 -3
  53. package/modules/@apostrophecms/widget-type/index.js +4 -0
  54. package/package.json +20 -3
  55. package/test/change-doc-ids.js +134 -0
  56. package/test/i18n.js +310 -0
  57. package/test/pieces-children/pieces-malformed-child.js +32 -0
  58. package/test/pieces-malformed.js +33 -0
  59. package/test/widgets-children/widgets-malformed-child.js +32 -0
  60. package/test/widgets-malformed.js +34 -0
  61. package/test/static-i18n.js +0 -105
@@ -193,6 +193,12 @@ module.exports = {
193
193
  if (!self.options.name) {
194
194
  self.options.name = self.__meta.name;
195
195
  }
196
+ if (self.options.singletonAuto) {
197
+ self.options.singleton = true;
198
+ }
199
+ if (self.options.replicate === undefined) {
200
+ self.options.replicate = self.options.localized && self.options.singletonAuto;
201
+ }
196
202
  self.name = self.options.name;
197
203
  // Each doc-type has an array of fields which will be updated
198
204
  // if the document is moved to the archive. In most cases 'slug'
@@ -508,6 +514,26 @@ module.exports = {
508
514
  //
509
515
  // `query.field` will contain the schema field definition for
510
516
  // the relationship the user is attempting to match titles from.
517
+ getRelationshipQueryBuilderChoicesProjection(query) {
518
+ const projection = self.getAutocompleteProjection(query);
519
+
520
+ return {
521
+ ...projection,
522
+ title: 1,
523
+ type: 1,
524
+ _id: 1,
525
+ _url: 1,
526
+ slug: 1
527
+ };
528
+ },
529
+ // Returns a MongoDB projection object to be used when querying
530
+ // for this type if all that is needed is a title for display
531
+ // in an autocomplete menu. Default behavior is to
532
+ // return only the `title`, `_id` and `slug` properties.
533
+ // Removing any of these three is not recommended.
534
+ //
535
+ // `query.field` will contain the schema field definition for
536
+ // the relationship the user is attempting to match titles from.
511
537
  getAutocompleteProjection(query) {
512
538
  return {
513
539
  title: 1,
@@ -524,6 +550,11 @@ module.exports = {
524
550
  // event start dates and similar information that helps the
525
551
  // user distinguish between docs.
526
552
  getAutocompleteTitle(doc, query) {
553
+ // TODO Remove in next major version.
554
+ self.apos.util.warnDevOnce(
555
+ 'deprecate-get-autocomplete-title',
556
+ 'self.getAutocompleteTitle() is deprecated. Use the autocomplete(\'...\') query builder instead. More info at https://v3.docs.apostrophecms.org/reference/query-builders.html#autocomplete'
557
+ );
527
558
  return doc.title;
528
559
  },
529
560
  // Used by `@apostrophecms/version` to label changes that
@@ -615,6 +646,12 @@ module.exports = {
615
646
  //
616
647
  // We don't launder the input here, see the 'autocomplete' route.
617
648
  async autocomplete(req, query) {
649
+ // TODO Remove in next major version.
650
+ self.apos.util.warnDevOnce(
651
+ 'deprecate-autocomplete',
652
+ 'self.autocomplete() is deprecated. Use the autocomplete(\'...\') query builder instead. More info at https://v3.docs.apostrophecms.org/reference/query-builders.html#autocomplete'
653
+ );
654
+
618
655
  const _query = query.find(req, {}).sort('search');
619
656
  if (query.extendAutocompleteQuery) {
620
657
  query.extendAutocompleteQuery(_query);
@@ -1442,7 +1479,9 @@ module.exports = {
1442
1479
  browserOptions.schema = self.allowedSchema(req);
1443
1480
  browserOptions.localized = self.isLocalized();
1444
1481
  browserOptions.autopublish = self.options.autopublish;
1445
- browserOptions.previewDraft = self.isLocalized() && !browserOptions.autopublish && self.options.previewDraft;
1482
+ browserOptions.previewDraft = self.isLocalized() &&
1483
+ !browserOptions.autopublish &&
1484
+ self.options.previewDraft;
1446
1485
 
1447
1486
  return browserOptions;
1448
1487
  }
@@ -42,7 +42,9 @@ module.exports = {
42
42
  // Intentionally the same
43
43
  pluralLabel: 'apostrophe:globalDocLabel',
44
44
  searchable: false,
45
- singleton: true,
45
+ singletonAuto: {
46
+ slug: 'global'
47
+ },
46
48
  showPermissions: true,
47
49
  replicate: true
48
50
  },
@@ -77,23 +79,10 @@ module.exports = {
77
79
  },
78
80
  methods(self) {
79
81
  return {
80
- async insertIfMissing() {
81
- // Insert at startup
82
- const req = self.apos.task.getReq();
83
- const existing = await self.apos.doc.db.findOne({ slug: self.slug });
84
- if (!existing) {
85
- const _new = self.newInstance();
86
- Object.assign(_new, {
87
- slug: self.slug,
88
- type: self.name
89
- });
90
- await self.insert(req, _new);
91
- }
92
- },
93
82
  // Fetch and return the `global` doc object. You probably don't need to call this,
94
83
  // because middleware has already populated `req.data.global` for you.
95
84
  async findGlobal(req) {
96
- return self.find(req, { slug: self.slug }).permission(false).toObject();
85
+ return self.find(req, { type: self.name }).permission(false).toObject();
97
86
  },
98
87
  // Fetch the global doc and add it to `req.data` as `req.data.global`, if it
99
88
  // is not already present. If it is already present, skip the
@@ -376,13 +376,13 @@
376
376
  "richTextH4": "Heading 4 (H4)",
377
377
  "richTextHighlight": "Mark",
378
378
  "richTextHorizontalRule": "Horizontal Rule",
379
- "richTextInsertMenuHeading": "Insert...",
379
+ "richTextInsertMenuHeading": "Insert element...",
380
380
  "richTextItalic": "Italic",
381
381
  "richTextLink": "Link",
382
382
  "richTextOrderedList": "Ordered List",
383
383
  "richTextParagraph": "Paragraph (P)",
384
384
  "richTextPlaceholder": "Start Typing Here...",
385
- "richTextPlaceholderWithInsertMenu": "Start Typing Here or Press /...",
385
+ "richTextPlaceholderWithInsertMenu": "Start typing or press '/' for commands...",
386
386
  "richTextRedo": "Redo",
387
387
  "richTextStrikethrough": "Strike",
388
388
  "richTextStyleConfigWarning": "Misconfigured rich text style: label: {{ label }}, {{ tag }}",
@@ -399,6 +399,7 @@
399
399
  "saveDraftDescription": "Save as a draft to publish later.",
400
400
  "saveType": "Save {{ type }}",
401
401
  "savingDocument": "Saving document...",
402
+ "search": "Search",
402
403
  "searchDocType": "Search {{ type }}",
403
404
  "searchLabel": "Search Page",
404
405
  "searchLocales": "Search Locales",
@@ -202,6 +202,47 @@ module.exports = {
202
202
  req.session.cookie = new ExpressSessionCookie(aposExpressModule.sessionOptions.cookie);
203
203
  return res.redirect(self.apos.url.build(req.url, { aposCrossDomainSessionToken: null }));
204
204
  },
205
+ // If the `redirectToFirstLocale` option is enabled
206
+ // and the homepage is requested,
207
+ // redirects to the first locale configured with the
208
+ // current requested hostname when all of the locales
209
+ // configured with that hostname do have a prefix.
210
+ //
211
+ // However, if the request does not match any explicit
212
+ // hostnames assigned to locales, redirects to the first
213
+ // locale that does not have a configured hostname, if
214
+ // all the locales without a hostname do have a prefix.
215
+ redirectToFirstLocale(req, res, next) {
216
+ if (!self.options.redirectToFirstLocale) {
217
+ return next();
218
+ }
219
+ if (req.path !== '' && req.path !== '/') {
220
+ return next();
221
+ }
222
+
223
+ const locales = Object.values(
224
+ self.filterPrivateLocales(req, self.locales)
225
+ );
226
+ const localesWithoutHostname = locales.filter(
227
+ locale => !locale.hostname
228
+ );
229
+ const localesWithCurrentHostname = locales.filter(
230
+ locale => locale.hostname && locale.hostname.split(':')[0] === req.hostname
231
+ );
232
+
233
+ const localesToCheck = localesWithCurrentHostname.length
234
+ ? localesWithCurrentHostname
235
+ : localesWithoutHostname;
236
+
237
+ if (!localesToCheck.length || !localesToCheck.every(locale => locale.prefix)) {
238
+ return next();
239
+ }
240
+
241
+ // Add / for home page and to avoid being redirected again in the `locale` middleware:
242
+ const redirectUrl = `${localesToCheck[0].prefix}/`;
243
+
244
+ return res.redirect(redirectUrl);
245
+ },
205
246
  locale(req, res, next) {
206
247
  // Support for a single aposLocale query param that
207
248
  // also contains the mode, which is likely to occur
@@ -666,6 +707,37 @@ module.exports = {
666
707
  .entries(locales)
667
708
  .filter(([ name, options ]) => options.private !== true)
668
709
  );
710
+ },
711
+ // Rename a locale. This is time consuming and should be
712
+ // avoided when possible. If `keep` is present it must be set
713
+ // to either `oldLocale` or `newLocale` and indicates which version
714
+ // is kept in the event of a conflict
715
+ async rename(oldLocale, newLocale, { keep } = {}) {
716
+ let renamed = 0;
717
+ let kept = 0;
718
+ if (!oldLocale) {
719
+ throw new Error('You must specify --old');
720
+ }
721
+ if (!newLocale) {
722
+ throw new Error('You must specify --new');
723
+ }
724
+ if (oldLocale === newLocale) {
725
+ throw new Error('The old and new locales must be different');
726
+ }
727
+ if (keep && (!(keep === oldLocale) && !(keep === newLocale))) {
728
+ throw new Error('--keep must match --old or --new');
729
+ }
730
+ const ids = await self.apos.doc.db.find({ aposLocale: new RegExp(`^${self.apos.util.regExpQuote(oldLocale)}:`) }).project({ _id: 1 }).toArray();
731
+ ({
732
+ renamed,
733
+ kept
734
+ } = await self.apos.doc.changeDocIds(ids.map(doc => [ doc._id, doc._id.replace(`:${oldLocale}`, `:${newLocale}`) ]), {
735
+ keep: (keep === oldLocale) ? 'old' : (keep === newLocale) ? 'new' : false
736
+ }));
737
+ return {
738
+ renamed,
739
+ kept
740
+ };
669
741
  }
670
742
  };
671
743
  },
@@ -677,67 +749,10 @@ module.exports = {
677
749
  const oldLocale = self.apos.launder.string(argv.old);
678
750
  const newLocale = self.apos.launder.string(argv.new);
679
751
  const keep = self.apos.launder.string(argv.keep);
680
- let renamed = 0;
681
- let kept = 0;
682
- if (!oldLocale) {
683
- throw new Error('You must specify --old');
684
- }
685
- if (!newLocale) {
686
- throw new Error('You must specify --new');
687
- }
688
- if (oldLocale === newLocale) {
689
- throw new Error('The old and new locales must be different');
690
- }
691
- if (keep && (!(keep === oldLocale) && !(keep === newLocale))) {
692
- throw new Error('--keep must match --old or --new');
693
- }
694
- await self.apos.migration.eachDoc({ aposLocale: new RegExp(`^${self.apos.util.regExpQuote(oldLocale)}:`) }, async doc => {
695
- const newDoc = {
696
- ...doc,
697
- aposLocale: doc.aposLocale.replace(oldLocale, newLocale),
698
- _id: doc._id.replace(`:${oldLocale}`, `:${newLocale}`)
699
- };
700
- try {
701
- // Remove old first to cut down on duplicate key conflicts due to
702
- // custom properties
703
- await self.apos.doc.db.removeOne({ _id: doc._id });
704
- await self.apos.doc.db.insertOne(newDoc);
705
- renamed++;
706
- } catch (e) {
707
- // First reinsert old doc to prevent content loss on new doc insert failure
708
- await self.apos.doc.db.insertOne(doc);
709
- if (!self.apos.doc.isUniqueError(e)) {
710
- throw e;
711
- }
712
- const existing = await self.apos.doc.db.findOne({ _id: newDoc._id });
713
- if (!existing) {
714
- // We don't know the cause of this error
715
- throw e;
716
- }
717
- if (keep === newLocale) {
718
- // New content already exists in new locale, delete old locale
719
- // and keep new
720
- await self.apos.doc.db.removeOne({ _id: doc._id });
721
- kept++;
722
- } else if (keep === oldLocale) {
723
- // We want to keep the old locale's content. Once again we
724
- // need to remove the old doc first to cut down on conflicts
725
- try {
726
- await self.apos.doc.db.removeOne({ _id: doc._id });
727
- await self.apos.doc.db.deleteOne({ _id: newDoc._id });
728
- await self.apos.doc.db.insertOne(newDoc);
729
- } catch (e) {
730
- // Reinsert old doc to prevent content loss on new doc insert failure
731
- await self.apos.doc.db.insertOne(doc);
732
- throw e;
733
- }
734
- kept++;
735
- } else {
736
- console.error('A conflict occurred. Use --keep to specify a locale to keep and retry');
737
- throw e;
738
- }
739
- }
740
- });
752
+ const {
753
+ renamed,
754
+ kept
755
+ } = await self.rename(oldLocale, newLocale, { keep });
741
756
  console.log(`Renamed ${renamed} documents from ${oldLocale} to ${newLocale}`);
742
757
  if (keep) {
743
758
  console.log(`Due to conflicts, kept ${kept} documents from ${keep}`);
@@ -462,6 +462,14 @@ module.exports = {
462
462
  },
463
463
  extendMethods(self) {
464
464
  return {
465
+ getRelationshipQueryBuilderChoicesProjection(_super, query) {
466
+ const projection = _super(query);
467
+
468
+ return {
469
+ ...projection,
470
+ attachment: 1
471
+ };
472
+ },
465
473
  getBrowserData(_super, req) {
466
474
  const data = _super(req);
467
475
  data.components.managerModal = 'AposMediaManager';
@@ -14,6 +14,7 @@
14
14
  class="apos-media-manager-display__cell" v-for="item in items"
15
15
  :key="idFor(item)"
16
16
  :class="{'apos-is-selected': checked.includes(item._id)}"
17
+ :style="getCellStyles(item)"
17
18
  >
18
19
  <div class="apos-media-manager-display__checkbox">
19
20
  <AposCheckbox
@@ -114,6 +115,10 @@ export default {
114
115
  type: String,
115
116
  required: false,
116
117
  default: null
118
+ },
119
+ largePreview: {
120
+ type: Boolean,
121
+ default: false
117
122
  }
118
123
  },
119
124
  emits: [
@@ -149,7 +154,7 @@ export default {
149
154
  const parentRatio = parentWidth / parentHeight;
150
155
  const itemRatio = item.dimensions.width / item.dimensions.height;
151
156
 
152
- if (parentRatio < itemRatio) {
157
+ if ((parentRatio < itemRatio) || this.largePreview) {
153
158
  return {
154
159
  width: `${item.dimensions.width}px`,
155
160
  paddingTop: `${(item.dimensions.height / item.dimensions.width) * 100}%`
@@ -162,6 +167,14 @@ export default {
162
167
  }
163
168
 
164
169
  },
170
+ getCellStyles(item) {
171
+ if (this.largePreview && item.dimensions) {
172
+ return {
173
+ width: `${item.dimensions.width}px`,
174
+ height: `${item.dimensions.height}px`
175
+ };
176
+ }
177
+ },
165
178
  addDragClass(event) {
166
179
  event.target.classList.add('apos-is-hovering');
167
180
  },
@@ -43,69 +43,12 @@
43
43
  </template>
44
44
 
45
45
  <script>
46
- import AposLoginFormMixin from 'Modules/@apostrophecms/login/mixins/AposLoginFormMixin';
46
+ import AposForgotPasswordFormLogic from 'Modules/@apostrophecms/login/logic/AposForgotPasswordForm';
47
47
 
48
48
  export default {
49
49
  name: 'AposForgotPasswordForm',
50
- mixins: [ AposLoginFormMixin ],
51
- emits: [ 'set-stage' ],
52
- data() {
53
- return {
54
- busy: false,
55
- done: false,
56
- schema: [
57
- {
58
- name: 'email',
59
- label: 'apostrophe:email',
60
- placeholder: 'apostrophe:loginEnterEmail',
61
- type: 'string',
62
- required: true
63
- }
64
- ]
65
- };
66
- },
67
- computed: {
68
- disabled() {
69
- return this.doc.hasErrors;
70
- },
71
- help() {
72
- if (this.done) {
73
- return this.$t('apostrophe:loginResetRequestDone', {
74
- email: this.doc.data.email
75
- });
76
- }
77
- return this.$t('apostrophe:loginResetPasswordRequest');
78
- }
79
- },
80
- created() {
81
- if (!this.passwordResetEnabled) {
82
- this.$emit('set-stage', 'login');
83
- }
84
- },
85
- methods: {
86
- async submit() {
87
- if (this.busy) {
88
- return;
89
- }
90
- this.busy = true;
91
- this.error = '';
92
-
93
- await this.requestReset();
94
- },
95
- async requestReset() {
96
- try {
97
- await apos.http.post(`${apos.login.action}/reset-request`, {
98
- busy: true,
99
- body: { ...this.doc.data }
100
- });
101
- this.done = true;
102
- } catch (e) {
103
- this.error = e.message || 'apostrophe:loginErrorGeneric';
104
- } finally {
105
- this.busy = false;
106
- }
107
- }
108
- }
50
+ mixins: [ AposForgotPasswordFormLogic ],
51
+ emits: [ 'set-stage' ]
109
52
  };
110
53
  </script>
111
54
 
@@ -70,241 +70,13 @@
70
70
  </template>
71
71
 
72
72
  <script>
73
- import AposLoginFormMixin from 'Modules/@apostrophecms/login/mixins/AposLoginFormMixin';
73
+ import AposLoginFormLogic from 'Modules/@apostrophecms/login/logic/AposLoginForm';
74
74
 
75
75
  export default {
76
76
  name: 'AposLoginForm',
77
- mixins: [ AposLoginFormMixin ],
78
- emits: [ 'redirect', 'set-stage' ],
79
- data() {
80
- return {
81
- phase: 'beforeSubmit',
82
- busy: false,
83
- schema: [
84
- {
85
- name: 'username',
86
- label: 'Username',
87
- placeholder: 'Enter username',
88
- type: 'string',
89
- required: true
90
- },
91
- {
92
- name: 'password',
93
- label: 'Password',
94
- placeholder: 'Enter password',
95
- type: 'password',
96
- required: true
97
- }
98
- ],
99
- requirements: getRequirements(),
100
- requirementProps: {},
101
- fetchingRequirementProps: false
102
- };
103
- },
104
- computed: {
105
- disabled() {
106
- return this.doc.hasErrors ||
107
- !!this.beforeSubmitRequirements.find(requirement => !requirement.done);
108
- },
109
- beforeSubmitRequirements() {
110
- return this.requirements.filter(requirement => requirement.phase === 'beforeSubmit');
111
- },
112
- // The currently active requirement expecting a solo presentation.
113
- // Currently it only concerns `afterPasswordVerified` requirements.
114
- // beforeSubmit requirements are not presented solo.
115
- activeSoloRequirement() {
116
- return (this.phase === 'afterPasswordVerified') &&
117
- this.requirements.find(requirement =>
118
- (requirement.phase === 'afterPasswordVerified') && !requirement.done
119
- );
120
- }
121
- },
122
- watch: {
123
- context(newVal) {
124
- this.requirementProps = newVal.requirementProps;
125
- },
126
- async activeSoloRequirement(newVal) {
127
- if (
128
- (this.phase === 'afterPasswordVerified') &&
129
- (newVal?.phase === 'afterPasswordVerified') &&
130
- newVal.propsRequired &&
131
- !(newVal.success || newVal.error)
132
- ) {
133
- try {
134
- this.fetchingRequirementProps = true;
135
- const data = await apos.http.post(`${apos.login.action}/requirement-props`, {
136
- busy: true,
137
- body: {
138
- name: newVal.name,
139
- incompleteToken: this.incompleteToken
140
- }
141
- });
142
- this.requirementProps = {
143
- ...this.requirementProps,
144
- [newVal.name]: data
145
- };
146
- } catch (e) {
147
- this.error = e.message || 'apostrophe:loginErrorGeneric';
148
- } finally {
149
- this.fetchingRequirementProps = false;
150
- }
151
- } else {
152
- return null;
153
- }
154
- }
155
- },
156
- created() {
157
- this.requirementProps = this.context.requirementProps;
158
- },
159
- methods: {
160
- async submit() {
161
- if (this.busy) {
162
- return;
163
- }
164
- this.busy = true;
165
- this.error = '';
166
-
167
- await this.invokeInitialLoginApi();
168
- },
169
- async invokeInitialLoginApi() {
170
- try {
171
- const response = await apos.http.post(`${apos.login.action}/login`, {
172
- busy: true,
173
- body: {
174
- ...this.doc.data,
175
- requirements: this.getInitialSubmitRequirementsData(),
176
- session: true
177
- }
178
- });
179
- if (response && response.incompleteToken) {
180
- this.incompleteToken = response.incompleteToken;
181
- this.phase = 'afterPasswordVerified';
182
- } else {
183
- this.redirectAfterLogin();
184
- }
185
- } catch (e) {
186
- this.error = e.message || 'An error occurred. Please try again.';
187
- this.phase = 'beforeSubmit';
188
- } finally {
189
- this.busy = false;
190
- }
191
- },
192
- getInitialSubmitRequirementsData() {
193
- return Object.fromEntries(this.requirements
194
- .filter(r => r.phase !== 'afterPasswordVerified' || !r.done)
195
- .map(r => ([
196
- r.name,
197
- r.value
198
- ])));
199
- },
200
- async invokeFinalLoginApi() {
201
- try {
202
- await apos.http.post(`${apos.login.action}/login`, {
203
- busy: true,
204
- body: {
205
- ...this.doc.data,
206
- incompleteToken: this.incompleteToken,
207
- requirements: this.getFinalSubmitRequirementsData(),
208
- session: true
209
- }
210
- });
211
- this.redirectAfterLogin();
212
- } catch (e) {
213
- this.error = e.message || 'An error occurred. Please try again.';
214
- this.phase = 'beforeSubmit';
215
- } finally {
216
- this.busy = false;
217
- }
218
- },
219
- getFinalSubmitRequirementsData() {
220
- return Object.fromEntries(this.requirements.filter(r => r.phase === 'afterPasswordVerified').map(r => ([
221
- r.name,
222
- r.value
223
- ])));
224
- },
225
- redirectAfterLogin() {
226
- // TODO handle situation where user should be sent somewhere other than homepage.
227
- // Redisplay homepage with editing interface
228
- this.$emit('redirect', `${apos.prefix}/`);
229
- },
230
- async requirementBlock(requirementBlock) {
231
- const requirement = this.requirements
232
- .find(requirement => requirement.name === requirementBlock.name);
233
- requirement.done = false;
234
- requirement.value = undefined;
235
- },
236
- async requirementDone(requirementDone, value) {
237
- const requirement = this.requirements
238
- .find(requirement => requirement.name === requirementDone.name);
239
-
240
- if (requirement.phase === 'beforeSubmit') {
241
- requirement.done = true;
242
- requirement.value = value;
243
- return;
244
- }
245
-
246
- requirement.error = null;
247
-
248
- try {
249
- await apos.http.post(`${apos.login.action}/requirement-verify`, {
250
- busy: true,
251
- body: {
252
- name: requirement.name,
253
- value,
254
- incompleteToken: this.incompleteToken
255
- }
256
- });
257
-
258
- requirement.success = true;
259
- } catch (err) {
260
- requirement.error = err;
261
- }
262
-
263
- // Avoids the need for a deep watch
264
- this.requirements = [ ...this.requirements ];
265
-
266
- if (requirement.success && !requirement.askForConfirmation) {
267
- requirement.done = true;
268
-
269
- if (!this.activeSoloRequirement) {
270
- await this.invokeFinalLoginApi();
271
- }
272
- }
273
- },
274
-
275
- async requirementConfirmed (requirementConfirmed) {
276
- const requirement = this.requirements
277
- .find(requirement => requirement.name === requirementConfirmed.name);
278
-
279
- requirement.done = true;
280
-
281
- if (!this.activeSoloRequirement) {
282
- await this.invokeFinalLoginApi();
283
- }
284
- },
285
- getRequirementProps(name) {
286
- return this.requirementProps[name] || {};
287
- }
288
- }
77
+ mixins: [ AposLoginFormLogic ],
78
+ emits: [ 'set-stage' ]
289
79
  };
290
-
291
- function getRequirements() {
292
- const requirements = Object.entries(apos.login.requirements).map(([ name, requirement ]) => {
293
- return {
294
- name,
295
- component: requirement.component || name,
296
- ...requirement,
297
- done: false,
298
- value: null,
299
- success: null,
300
- error: null
301
- };
302
- });
303
- return [
304
- ...requirements.filter(r => r.phase === 'beforeSubmit'),
305
- ...requirements.filter(r => r.phase === 'afterPasswordVerified')
306
- ];
307
- }
308
80
  </script>
309
81
 
310
82
  <style lang="scss" scoped>