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.
- package/CHANGELOG.md +73 -2
- package/index.js +20 -2
- package/lib/locales.js +1 -1
- package/lib/moog-require.js +7 -0
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +12 -2
- package/modules/@apostrophecms/any-page-type/index.js +5 -0
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaEditor.vue +2 -0
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +7 -24
- package/modules/@apostrophecms/asset/index.js +27 -2
- package/modules/@apostrophecms/asset/lib/webpack/apos/webpack.config.js +23 -2
- package/modules/@apostrophecms/asset/lib/webpack/src/webpack.config.js +26 -2
- package/modules/@apostrophecms/doc/index.js +149 -0
- package/modules/@apostrophecms/doc-type/index.js +40 -1
- package/modules/@apostrophecms/global/index.js +4 -15
- package/modules/@apostrophecms/i18n/i18n/en.json +3 -2
- package/modules/@apostrophecms/i18n/index.js +76 -61
- package/modules/@apostrophecms/image/index.js +8 -0
- package/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerDisplay.vue +14 -1
- package/modules/@apostrophecms/login/ui/apos/components/AposForgotPasswordForm.vue +3 -60
- package/modules/@apostrophecms/login/ui/apos/components/AposLoginForm.vue +3 -231
- package/modules/@apostrophecms/login/ui/apos/components/AposResetPasswordForm.vue +3 -96
- package/modules/@apostrophecms/login/ui/apos/components/TheAposLogin.vue +2 -99
- package/modules/@apostrophecms/login/ui/apos/logic/AposForgotPasswordForm.js +68 -0
- package/modules/@apostrophecms/login/ui/apos/logic/AposLoginForm.js +239 -0
- package/modules/@apostrophecms/login/ui/apos/logic/AposResetPasswordForm.js +105 -0
- package/modules/@apostrophecms/login/ui/apos/logic/TheAposLogin.js +107 -0
- package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +9 -3
- package/modules/@apostrophecms/modal/ui/apos/components/AposModalToolbar.vue +1 -0
- package/modules/@apostrophecms/page/index.js +63 -1
- package/modules/@apostrophecms/page-type/index.js +6 -0
- package/modules/@apostrophecms/piece-type/index.js +93 -9
- package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +4 -0
- package/modules/@apostrophecms/rich-text-widget/index.js +1 -1
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposImageControlDialog.vue +14 -10
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +252 -86
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapAnchor.vue +0 -1
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapLink.vue +0 -1
- package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Image.js +76 -54
- package/modules/@apostrophecms/schema/index.js +1 -2
- package/modules/@apostrophecms/schema/lib/addFieldTypes.js +35 -7
- package/modules/@apostrophecms/schema/ui/apos/components/AposArrayEditor.vue +0 -1
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputArray.vue +0 -1
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputObject.vue +0 -1
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputSlug.vue +21 -1
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputString.vue +12 -7
- package/modules/@apostrophecms/schema/ui/apos/components/AposSchema.vue +7 -1
- package/modules/@apostrophecms/ui/ui/apos/components/AposCombo.vue +178 -20
- package/modules/@apostrophecms/ui/ui/apos/components/AposFilterMenu.vue +1 -1
- package/modules/@apostrophecms/ui/ui/apos/components/AposPager.vue +4 -6
- package/modules/@apostrophecms/ui/ui/apos/scss/mixins/_theme_mixins.scss +1 -0
- package/modules/@apostrophecms/util/index.js +5 -6
- package/modules/@apostrophecms/util/ui/src/http.js +6 -3
- package/modules/@apostrophecms/widget-type/index.js +4 -0
- package/package.json +20 -3
- package/test/change-doc-ids.js +134 -0
- package/test/i18n.js +310 -0
- package/test/pieces-children/pieces-malformed-child.js +32 -0
- package/test/pieces-malformed.js +33 -0
- package/test/widgets-children/widgets-malformed-child.js +32 -0
- package/test/widgets-malformed.js +34 -0
- 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() &&
|
|
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
|
-
|
|
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, {
|
|
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
|
|
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
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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
|
|
46
|
+
import AposForgotPasswordFormLogic from 'Modules/@apostrophecms/login/logic/AposForgotPasswordForm';
|
|
47
47
|
|
|
48
48
|
export default {
|
|
49
49
|
name: 'AposForgotPasswordForm',
|
|
50
|
-
mixins: [
|
|
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
|
|
73
|
+
import AposLoginFormLogic from 'Modules/@apostrophecms/login/logic/AposLoginForm';
|
|
74
74
|
|
|
75
75
|
export default {
|
|
76
76
|
name: 'AposLoginForm',
|
|
77
|
-
mixins: [
|
|
78
|
-
emits: [ '
|
|
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>
|