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
@@ -0,0 +1,107 @@
1
+ // This is the business logic of the TheAposLogin Vue component.
2
+ // It is in a separate file so that you can override the component's templates
3
+ // and styles just by copying the .vue file to your project, and leave the business logic
4
+ // unchanged.
5
+
6
+ import AposThemeMixin from 'Modules/@apostrophecms/ui/mixins/AposThemeMixin';
7
+
8
+ const STAGES = [
9
+ 'login',
10
+ 'forgotPassword',
11
+ 'resetPassword'
12
+ ];
13
+
14
+ export default {
15
+ mixins: [ AposThemeMixin ],
16
+ data() {
17
+ return {
18
+ stage: STAGES[0],
19
+ mounted: false,
20
+ beforeCreateFinished: false,
21
+ error: '',
22
+ passwordResetData: {},
23
+ context: {}
24
+ };
25
+ },
26
+ computed: {
27
+ loaded() {
28
+ return this.mounted && this.beforeCreateFinished;
29
+ },
30
+ showNav() {
31
+ return this.stage !== STAGES[0];
32
+ },
33
+ homeUrl() {
34
+ return `${apos.prefix}/`;
35
+ }
36
+ },
37
+ // We need it here and not in the login form because the version used in the footer.
38
+ // The context will be passed to every form, might be a good thing in the future.
39
+ async beforeCreate() {
40
+ const stateChange = parseInt(window.sessionStorage.getItem('aposStateChange'));
41
+ const seen = JSON.parse(window.sessionStorage.getItem('aposStateChangeSeen') || '{}');
42
+ if (!seen[window.location.href]) {
43
+ const lastModified = Date.parse(document.lastModified);
44
+ if (stateChange && lastModified && (lastModified < stateChange)) {
45
+ seen[window.location.href] = true;
46
+ window.sessionStorage.setItem('aposStateChangeSeen', JSON.stringify(seen));
47
+ location.reload();
48
+ return;
49
+ }
50
+ }
51
+ try {
52
+ this.context = await apos.http.post(`${apos.login.action}/context`, {
53
+ busy: true
54
+ });
55
+ } catch (e) {
56
+ this.context = {};
57
+ this.error = e.message || 'apostrophe:loginErrorGeneric';
58
+ } finally {
59
+ this.beforeCreateFinished = true;
60
+ }
61
+ },
62
+ created() {
63
+ const url = new URL(document.location);
64
+ const data = {
65
+ email: url.searchParams.get('email'),
66
+ reset: url.searchParams.get('reset')
67
+ };
68
+ if (data.email && data.reset) {
69
+ this.passwordResetData = data;
70
+ this.setStage('resetPassword');
71
+ }
72
+ },
73
+ mounted() {
74
+ this.mounted = true;
75
+ },
76
+ methods: {
77
+ setStage(name) {
78
+ // 1. Enabled status per stage. A bit cryptic but effective.
79
+ // Search for a method composed of the `name` + `Enabled`
80
+ // (e.g. `forgotPasswordEnabled` and execute it (should return boolean).
81
+ // If no method is found it is enabled. Fallback to the default stage.
82
+ const enabled = this[`${name}Enabled`]?.() ?? true;
83
+ if (!enabled) {
84
+ this.stage = STAGES[0];
85
+ return;
86
+ }
87
+ // 2. Set it only if it's a known stage
88
+ if (STAGES.includes(name)) {
89
+ this.stage = name;
90
+ return;
91
+ }
92
+ // 3. Fallback to the default stage
93
+ this.stage = STAGES[0];
94
+ },
95
+ forgotPasswordEnabled() {
96
+ return apos.login.passwordResetEnabled;
97
+ },
98
+ resetPasswordEnabled() {
99
+ return apos.login.passwordResetEnabled;
100
+ },
101
+ onRedirect(loc) {
102
+ window.sessionStorage.setItem('aposStateChange', Date.now());
103
+ window.sessionStorage.setItem('aposStateChangeSeen', '{}');
104
+ location.assign(loc);
105
+ }
106
+ }
107
+ };
@@ -5,6 +5,7 @@
5
5
  v-if="canSelectAll"
6
6
  label="apostrophe:select"
7
7
  type="outline"
8
+ :modifiers="['small']"
8
9
  text-color="var(--a-base-1)"
9
10
  :icon-only="true"
10
11
  :icon="checkboxIcon"
@@ -24,9 +25,9 @@
24
25
  <AposButton
25
26
  v-if="!operations"
26
27
  :label="label"
27
- :icon-only="true"
28
28
  :icon="icon"
29
29
  :disabled="!checkedCount"
30
+ :modifiers="['small']"
30
31
  type="outline"
31
32
  @click="confirmOperation({ action, label, ...rest })"
32
33
  />
@@ -302,7 +303,12 @@ export default {
302
303
  </script>
303
304
 
304
305
  <style lang="scss" scoped>
305
- .apos-manager-toolbar ::v-deep .apos-field--search {
306
- width: 250px;
306
+ .apos-manager-toolbar ::v-deep {
307
+ .apos-field--search {
308
+ width: 250px;
309
+ }
310
+ .apos-input {
311
+ height: 32px;
312
+ }
307
313
  }
308
314
  </style>
@@ -45,5 +45,6 @@ export default {
45
45
 
46
46
  .apos-toolbar__group {
47
47
  display: flex;
48
+ align-items: center;
48
49
  }
49
50
  </style>
@@ -100,6 +100,7 @@ module.exports = {
100
100
  self.enableBrowserData();
101
101
  self.addLegacyMigrations();
102
102
  self.addMisreplicatedParkedPagesMigration();
103
+ self.addDuplicateParkedPagesMigration();
103
104
  await self.createIndexes();
104
105
  },
105
106
  restApiRoutes(self) {
@@ -1959,7 +1960,10 @@ database.`);
1959
1960
  delete _item._children;
1960
1961
  if (!parent) {
1961
1962
  // Parking the home page for the first time
1962
- _item.aposDocId = self.apos.util.generateId();
1963
+ _item.aposDocId = await self.apos.doc.bestAposDocId({
1964
+ level: 0,
1965
+ slug: '/'
1966
+ });
1963
1967
  _item.path = _item.aposDocId;
1964
1968
  _item.lastPublishedAt = new Date();
1965
1969
  return self.apos.doc.insert(req, _item);
@@ -2365,6 +2369,64 @@ database.`);
2365
2369
  }
2366
2370
  }
2367
2371
  });
2372
+ },
2373
+ addDuplicateParkedPagesMigration() {
2374
+ self.apos.migration.add('duplicate-parked-pages', async () => {
2375
+ let parkedPages = await self.apos.doc.db.find({
2376
+ parkedId: {
2377
+ $ne: null
2378
+ }
2379
+ }).toArray();
2380
+ const parkedIds = [ ...new Set(parkedPages.map(page => page.parkedId)) ];
2381
+ const names = Object.keys(self.apos.i18n.locales);
2382
+ const locales = [
2383
+ ...names.map(locale => `${locale}:draft`),
2384
+ ...names.map(locale => `${locale}:published`),
2385
+ ...names.map(locale => `${locale}:previous`)
2386
+ ];
2387
+ let changes = 0;
2388
+ const winners = new Map();
2389
+ for (const locale of locales) {
2390
+ for (const parkedId of parkedIds) {
2391
+ let matches = parkedPages.filter(page =>
2392
+ (page.parkedId === parkedId) &&
2393
+ (page.aposLocale === locale)
2394
+ );
2395
+ if (matches.length > 0) {
2396
+ if (!winners.has(parkedId)) {
2397
+ winners.set(parkedId, matches[0].aposDocId);
2398
+ }
2399
+ }
2400
+ if (matches.length > 1) {
2401
+ matches = matches.sort((a, b) => a.createdAt - b.createdAt);
2402
+ const ids = matches.slice(1).map(page => page._id);
2403
+ await self.apos.doc.db.removeMany({
2404
+ _id: {
2405
+ $in: ids
2406
+ }
2407
+ });
2408
+ parkedPages = parkedPages.filter(page => !ids.includes(page._id));
2409
+ changes++;
2410
+ }
2411
+ }
2412
+ }
2413
+ const idChanges = [];
2414
+ for (const parkedId of parkedIds) {
2415
+ const aposDocId = winners.get(parkedId);
2416
+ const matches = parkedPages.filter(page => page.parkedId === parkedId);
2417
+ for (const match of matches) {
2418
+ if (match.aposDocId !== aposDocId) {
2419
+ idChanges.push([ match._id, match._id.replace(match.aposDocId, aposDocId) ]);
2420
+ }
2421
+ }
2422
+ }
2423
+ if (idChanges.length) {
2424
+ // Also calls self.apos.attachment.recomputeAllDocReferences
2425
+ await self.apos.doc.changeDocIds(idChanges);
2426
+ } else if (changes > 0) {
2427
+ await self.apos.attachment.recomputeAllDocReferences();
2428
+ }
2429
+ });
2368
2430
  }
2369
2431
  };
2370
2432
  },
@@ -22,6 +22,7 @@ module.exports = {
22
22
  type: 'select',
23
23
  label: 'apostrophe:type',
24
24
  required: true,
25
+ def: self.options.apos.page.typeChoices[0].name,
25
26
  choices: self.options.apos.page.typeChoices.map(function (type) {
26
27
  return {
27
28
  value: type.name,
@@ -297,6 +298,11 @@ module.exports = {
297
298
  // the `title` property, but since this is a page we are including
298
299
  // the slug as well.
299
300
  getAutocompleteTitle(doc, query) {
301
+ // TODO Remove in next major version.
302
+ self.apos.util.warnDevOnce(
303
+ 'deprecate-get-autocomplete-title',
304
+ 'self.getAutocompleteTitle() is deprecated. Use the autocomplete(\'...\') query builder instead. More info at https://v3.docs.apostrophecms.org/reference/query-builders.html#autocomplete'
305
+ );
300
306
  return doc.title + ' (' + doc.slug + ')';
301
307
  },
302
308
  // `req` determines what the user is eligible to edit, `criteria`
@@ -26,17 +26,39 @@ module.exports = {
26
26
  // publicApiProjection: {
27
27
  // title: 1,
28
28
  // _url: 1,
29
+ // },
30
+ // By default the manager modal will get all the pieces fields below + all manager columns
31
+ // you can enable a projection using
32
+ // managerApiProjection: {
33
+ // _id: 1,
34
+ // _url: 1,
35
+ // aposDocId: 1,
36
+ // aposLocale: 1,
37
+ // aposMode: 1,
38
+ // docPermissions: 1,
39
+ // slug: 1,
40
+ // title: 1,
41
+ // type: 1,
42
+ // visibility: 1
29
43
  // }
30
44
  },
31
- fields: {
32
- add: {
33
- slug: {
34
- type: 'slug',
35
- label: 'apostrophe:slug',
36
- following: [ 'title', 'archived' ],
37
- required: true
38
- }
39
- }
45
+ fields(self) {
46
+ return {
47
+ add: {
48
+ slug: {
49
+ type: 'slug',
50
+ label: 'apostrophe:slug',
51
+ following: [ 'title', 'archived' ],
52
+ required: true
53
+ }
54
+ },
55
+ remove: self.options.singletonAuto ? [
56
+ 'title',
57
+ 'slug',
58
+ 'archived',
59
+ 'visibility'
60
+ ] : []
61
+ };
40
62
  },
41
63
  columns(self) {
42
64
  return {
@@ -182,6 +204,10 @@ module.exports = {
182
204
  if (!self.options.name) {
183
205
  throw new Error('@apostrophecms/pieces require name option');
184
206
  }
207
+ const badFieldName = Object.keys(self.fields).indexOf('type') !== -1;
208
+ if (badFieldName) {
209
+ throw new Error(`The ${self.__meta.name} module contains a forbidden field property name: "type".`);
210
+ }
185
211
  if (!self.options.label) {
186
212
  // Englishify it
187
213
  self.options.label = _.startCase(self.options.name);
@@ -1063,7 +1089,49 @@ module.exports = {
1063
1089
  return self.apos.permission.can(req, batchOperation.permission, self.name);
1064
1090
  }
1065
1091
  return true;
1092
+
1066
1093
  });
1094
+ },
1095
+ getManagerApiProjection(req) {
1096
+ if (!self.options.managerApiProjection) {
1097
+ return null;
1098
+ }
1099
+
1100
+ const projection = { ...self.options.managerApiProjection };
1101
+ self.columns.forEach(({ name }) => {
1102
+ const column = (name.startsWith('draft:') || name.startsWith('published:'))
1103
+ ? name.replace(/^(draft|published):/, '')
1104
+ : name;
1105
+
1106
+ projection[column] = 1;
1107
+ });
1108
+
1109
+ return projection;
1110
+ },
1111
+ async insertIfMissing() {
1112
+ if (!self.options.singletonAuto) {
1113
+ return;
1114
+ }
1115
+ // Insert at startup
1116
+ const req = self.apos.task.getReq();
1117
+ const criteria = {
1118
+ type: self.name
1119
+ };
1120
+ if (self.options.localized) {
1121
+ criteria.aposLocale = {
1122
+ $in: Object.keys(self.apos.i18n.locales).map(locale => [ `${locale}:published`, `${locale}:draft` ]).flat()
1123
+ };
1124
+ }
1125
+ const existing = await self.apos.doc.db.findOne(criteria, { _id: 1 });
1126
+ if (!existing) {
1127
+ const _new = {
1128
+ ...self.newInstance(),
1129
+ aposDocId: await self.apos.doc.bestAposDocId({
1130
+ type: self.name
1131
+ })
1132
+ };
1133
+ await self.insert(req, _new);
1134
+ }
1067
1135
  }
1068
1136
  };
1069
1137
  },
@@ -1092,11 +1160,27 @@ module.exports = {
1092
1160
  editorModal: 'AposDocEditor',
1093
1161
  managerModal: 'AposDocsManager'
1094
1162
  });
1163
+ browserOptions.managerApiProjection = self.getManagerApiProjection(req);
1095
1164
 
1096
1165
  return browserOptions;
1097
1166
  },
1098
1167
  find(_super, req, criteria, projection) {
1099
1168
  return _super(req, criteria, projection).defaultSort(self.options.sort || { updatedAt: -1 });
1169
+ },
1170
+ newInstance(_super) {
1171
+ if (!self.options.singletonAuto) {
1172
+ return _super();
1173
+ }
1174
+ const slug = self.apos.util.slugify(self.options.singletonAuto?.slug || self.name);
1175
+ return {
1176
+ ..._super(),
1177
+ // These fields are removed from the editable schema of singletons,
1178
+ // but we assign them directly for broader compatibility
1179
+ slug,
1180
+ title: slug,
1181
+ archived: false,
1182
+ visibility: 'public'
1183
+ };
1100
1184
  }
1101
1185
  };
1102
1186
  },
@@ -316,6 +316,10 @@ export default {
316
316
  const {
317
317
  currentPage, pages, results, choices
318
318
  } = await this.request({
319
+ ...(
320
+ this.moduleOptions.managerApiProjection &&
321
+ { project: this.moduleOptions.managerApiProjection }
322
+ ),
319
323
  page: this.currentPage
320
324
  });
321
325
 
@@ -435,7 +435,7 @@ module.exports = {
435
435
  },
436
436
  {
437
437
  tag: 'img',
438
- attributes: [ 'src' ]
438
+ attributes: [ 'src', 'alt' ]
439
439
  }
440
440
  ]
441
441
  };
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <div
3
3
  v-if="active"
4
- v-click-outside-element="close"
4
+ v-click-outside-element="cancel"
5
5
  class="apos-popover apos-image-control__dialog"
6
6
  x-placement="bottom"
7
7
  :class="{
@@ -16,7 +16,6 @@
16
16
  :schema="schema"
17
17
  :trigger-validation="triggerValidation"
18
18
  v-model="docFields"
19
- :utility-rail="false"
20
19
  :modifiers="formModifiers"
21
20
  :key="lastSelectionTime"
22
21
  :generation="generation"
@@ -26,7 +25,7 @@
26
25
  <footer class="apos-image-control__footer">
27
26
  <AposButton
28
27
  type="default" label="apostrophe:cancel"
29
- @click="close"
28
+ @click="cancel"
30
29
  :modifiers="formModifiers"
31
30
  />
32
31
  <AposButton
@@ -55,7 +54,7 @@ export default {
55
54
  required: true
56
55
  }
57
56
  },
58
- emits: [ 'before-commands', 'close' ],
57
+ emits: [ 'before-commands', 'done', 'cancel' ],
59
58
  data() {
60
59
  return {
61
60
  generation: 1,
@@ -114,8 +113,11 @@ export default {
114
113
  }
115
114
  },
116
115
  methods: {
117
- close() {
118
- this.$emit('close');
116
+ cancel() {
117
+ this.$emit('cancel');
118
+ },
119
+ done() {
120
+ this.$emit('done');
119
121
  },
120
122
  save() {
121
123
  this.triggerValidation = true;
@@ -125,23 +127,25 @@ export default {
125
127
  }
126
128
  const image = this.docFields.data._image[0];
127
129
  this.docFields.data.imageId = image && image.aposDocId;
130
+ this.docFields.data.alt = image && image.alt;
128
131
  this.$emit('before-commands');
129
132
  this.editor.commands.setImage({
130
133
  imageId: this.docFields.data.imageId,
131
134
  caption: this.docFields.data.caption,
132
- style: this.docFields.data.style
135
+ style: this.docFields.data.style,
136
+ alt: this.docFields.data.alt
133
137
  });
134
- this.close();
138
+ this.done();
135
139
  });
136
140
  },
137
141
  keyboardHandler(e) {
138
142
  if (e.keyCode === 27) {
139
- this.close();
143
+ this.cancel();
140
144
  }
141
145
  if (e.keyCode === 13) {
142
146
  if (this.docFields.data.href || e.metaKey) {
143
147
  this.save();
144
- this.close();
148
+ this.done();
145
149
  }
146
150
  e.preventDefault();
147
151
  }