apostrophe 4.0.0 → 4.1.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 +24 -0
  2. package/defaults.js +2 -1
  3. package/modules/@apostrophecms/area/ui/apos/components/AposWidgetControls.vue +10 -10
  4. package/modules/@apostrophecms/attachment/index.js +2 -1
  5. package/modules/@apostrophecms/attachment/public/img/missing-icon.svg +14 -0
  6. package/modules/@apostrophecms/command-menu/ui/apos/components/AposCommandMenuKey.vue +1 -1
  7. package/modules/@apostrophecms/doc-type/index.js +34 -13
  8. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocContextMenu.vue +3 -3
  9. package/modules/@apostrophecms/i18n/i18n/en.json +13 -0
  10. package/modules/@apostrophecms/i18n/ui/apos/components/AposI18nLocalize.vue +173 -6
  11. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerSelections.vue +3 -2
  12. package/modules/@apostrophecms/login/index.js +18 -1
  13. package/modules/@apostrophecms/login/ui/apos/components/AposResetPasswordForm.vue +2 -2
  14. package/modules/@apostrophecms/login/ui/apos/logic/AposLoginForm.js +1 -16
  15. package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +9 -9
  16. package/modules/@apostrophecms/modal/ui/apos/components/AposModalConfirm.vue +4 -4
  17. package/modules/@apostrophecms/modal/ui/apos/components/AposModalLip.vue +2 -2
  18. package/modules/@apostrophecms/modal/ui/apos/components/AposModalTabs.vue +4 -3
  19. package/modules/@apostrophecms/notification/ui/apos/components/TheAposNotifications.vue +4 -2
  20. package/modules/@apostrophecms/oembed-field/ui/apos/components/AposInputOembed.vue +8 -6
  21. package/modules/@apostrophecms/page/index.js +1 -0
  22. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManagerSelectBox.vue +9 -5
  23. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapButton.vue +1 -1
  24. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapImage.vue +1 -1
  25. package/modules/@apostrophecms/schema/index.js +69 -8
  26. package/modules/@apostrophecms/schema/lib/addFieldTypes.js +1 -1
  27. package/modules/@apostrophecms/schema/ui/apos/components/AposInputPassword.vue +6 -4
  28. package/modules/@apostrophecms/schema/ui/apos/components/AposInputRange.vue +9 -6
  29. package/modules/@apostrophecms/schema/ui/apos/components/AposInputString.vue +1 -1
  30. package/modules/@apostrophecms/schema/ui/apos/components/AposInputWrapper.vue +15 -12
  31. package/modules/@apostrophecms/schema/ui/apos/components/AposSchema.vue +28 -19
  32. package/modules/@apostrophecms/schema/ui/apos/components/AposSearchList.vue +1 -1
  33. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputRelationship.js +1 -1
  34. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputSelect.js +2 -2
  35. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputString.js +2 -2
  36. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputWrapper.js +4 -4
  37. package/modules/@apostrophecms/schema/ui/apos/logic/AposSchema.js +4 -1
  38. package/modules/@apostrophecms/settings/ui/apos/components/AposSettingsManager.vue +1 -1
  39. package/modules/@apostrophecms/task/index.js +2 -0
  40. package/modules/@apostrophecms/translation/index.js +233 -0
  41. package/modules/@apostrophecms/translation/ui/apos/components/AposTranslationIndicator.vue +84 -0
  42. package/modules/@apostrophecms/ui/ui/apos/components/AposAvatar.vue +2 -1
  43. package/modules/@apostrophecms/ui/ui/apos/components/AposCellButton.vue +2 -1
  44. package/modules/@apostrophecms/ui/ui/apos/components/AposCellLabels.vue +49 -5
  45. package/modules/@apostrophecms/ui/ui/apos/components/AposCloudUploadIcon.vue +10 -5
  46. package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenu.vue +3 -5
  47. package/modules/@apostrophecms/ui/ui/apos/components/AposEmptyState.vue +3 -3
  48. package/modules/@apostrophecms/ui/ui/apos/components/AposIndicator.vue +1 -1
  49. package/modules/@apostrophecms/ui/ui/apos/components/AposLabel.vue +1 -1
  50. package/modules/@apostrophecms/ui/ui/apos/components/AposPagerDots.vue +2 -1
  51. package/modules/@apostrophecms/ui/ui/apos/components/AposSlat.vue +11 -10
  52. package/modules/@apostrophecms/ui/ui/apos/components/AposSpinner.vue +2 -2
  53. package/modules/@apostrophecms/ui/ui/apos/components/AposTag.vue +3 -2
  54. package/modules/@apostrophecms/ui/ui/apos/components/AposTagListItem.vue +2 -1
  55. package/modules/@apostrophecms/ui/ui/apos/components/AposTreeRows.vue +1 -1
  56. package/modules/@apostrophecms/ui/ui/apos/scss/global/_theme.scss +1 -0
  57. package/package.json +3 -3
  58. package/test/attachments.js +5 -0
  59. package/test/schemas.js +138 -0
  60. package/test/translation.js +538 -0
  61. package/test-lib/util.js +21 -0
package/CHANGELOG.md CHANGED
@@ -1,12 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.1.0 (2024-03-20)
4
+
5
+ ### Fixes
6
+
7
+ * Don't crash if a document of a type no longer corresponding to any module is present
8
+ together with the advanced permission module.
9
+ * AposLoginForm.js now pulls its schema from the user module rather than hardcoding it. Includes the
10
+ addition of `enterUsername` and `enterPassword` i18n fields for front end customization and localization.
11
+ * Simulated Express requests returned by `apos.task.getReq` now include a `req.headers` property, for
12
+ greater accuracy and to prevent unexpected bugs in other code.
13
+ * Fix the missing attachment icon. The responsibility for checking whether an attachment
14
+ actually exists before calling `attachment.url` still lies with the developer.
15
+
16
+ ### Adds
17
+
18
+ * Add new `getChanges` method to the schema module to get an array of document changed field names instead of just a boolean like does the `isEqual` method.
19
+ * Add highlight class in UI when comparing documents.
20
+
3
21
  ## 4.0.0 (2024-03-12)
4
22
 
5
23
  ### Adds
6
24
 
7
25
  * Add translation keys used by the multisite assembly module.
8
26
  * Add side by side comparison support in AposSchema component.
27
+ * Add `beforeLocalize` and `afterLocalize` events.
28
+ * Add custom manager indicators support via `apos.schema.addManagerIndicator({ component, props, if })`. The component registered this way will be automatically rendered in the manager modal.
9
29
  * Add the possibility to make widget modals wider, which can be useful for widgets that contain areas taking significant space. See [documentation](https://v3.docs.apostrophecms.org/reference/modules/widget-type.html#options).
30
+ * Temporarily add `translation` module to support document translations via the `@apostrophecms-pro/automatic-translation` module.
31
+ **The `translation` core module may be removed or refactored to reduce overhead in the core,** so its presence should
32
+ not be relied upon.
10
33
 
11
34
  ### Changes
12
35
 
@@ -19,6 +42,7 @@ as noted in our announcement and on the migration page of our website.
19
42
 
20
43
  * Adds `textStyle` to Tiptap types so that spans are rendered on RT initialization
21
44
  * `field.help` and `field.htmlHelp` are now correctly translated when displayed in a tooltip.
45
+ * Bump the `he` package to most recent version.
22
46
  * Notification REST APIs should not directly return the result of MongoDB operations.
23
47
 
24
48
  ## 3.63.2 (2024-03-01)
package/defaults.js CHANGED
@@ -55,6 +55,7 @@ module.exports = {
55
55
  '@apostrophecms/file-tag': {},
56
56
  '@apostrophecms/soft-redirect': {},
57
57
  '@apostrophecms/submitted-draft': {},
58
- '@apostrophecms/command-menu': {}
58
+ '@apostrophecms/command-menu': {},
59
+ '@apostrophecms/translation': {}
59
60
  }
60
61
  };
@@ -7,75 +7,75 @@
7
7
  v-if="!foreign"
8
8
  v-bind="upButton"
9
9
  :disabled="first || disabled"
10
- @click="$emit('up')"
11
10
  :tooltip="{
12
11
  content: (!disabled && !first) ? 'apostrophe:nudgeUp' : null,
13
12
  placement: 'left'
14
13
  }"
15
14
  :modifiers="[ 'inline' ]"
15
+ @click="$emit('up')"
16
16
  />
17
17
  <AposButton
18
+ v-if="!foreign && !options.contextual"
18
19
  v-bind="editButton"
19
20
  :disabled="disabled"
20
- v-if="!foreign && !options.contextual"
21
- @click="$emit('edit')"
22
21
  :tooltip="{
23
22
  content: 'apostrophe:editWidget',
24
23
  placement: 'left'
25
24
  }"
26
25
  :modifiers="[ 'inline' ]"
26
+ @click="$emit('edit')"
27
27
  />
28
28
  <AposButton
29
- v-bind="cutButton"
30
29
  v-if="!foreign"
31
- @click="$emit('cut')"
30
+ v-bind="cutButton"
32
31
  :tooltip="{
33
32
  content: 'apostrophe:cut',
34
33
  placement: 'left'
35
34
  }"
36
35
  :modifiers="[ 'inline' ]"
36
+ @click="$emit('cut')"
37
37
  />
38
38
  <AposButton
39
- v-bind="copyButton"
40
39
  v-if="!foreign"
41
- @click="$emit('copy')"
40
+ v-bind="copyButton"
42
41
  :tooltip="{
43
42
  content: 'apostrophe:copy',
44
43
  placement: 'left'
45
44
  }"
45
+ @click="$emit('copy')"
46
46
  />
47
47
  <AposButton
48
48
  v-if="!foreign"
49
49
  v-bind="cloneButton"
50
50
  :disabled="disabled || maxReached"
51
- @click="$emit('clone')"
52
51
  :tooltip="{
53
52
  content: 'apostrophe:duplicate',
54
53
  placement: 'left'
55
54
  }"
56
55
  :modifiers="[ 'inline' ]"
56
+ @click="$emit('clone')"
57
57
  />
58
58
  <AposButton
59
59
  v-if="!foreign"
60
60
  v-bind="removeButton"
61
61
  :disabled="disabled"
62
- @click="$emit('remove')"
63
62
  :tooltip="{
64
63
  content: 'apostrophe:delete',
65
64
  placement: 'left'
66
65
  }"
67
66
  :modifiers="[ 'inline' ]"
67
+ @click="$emit('remove')"
68
68
  />
69
69
  <AposButton
70
70
  v-if="!foreign"
71
71
  v-bind="downButton"
72
72
  :disabled="last || disabled"
73
- @click="$emit('down')"
74
73
  :tooltip="{
75
74
  content: (!disabled && !last) ? 'apostrophe:nudgeDown' : null,
76
75
  placement: 'left'
77
76
  }"
78
77
  :modifiers="[ 'inline' ]"
78
+ @click="$emit('down')"
79
79
  />
80
80
  </AposButtonGroup>
81
81
  </div>
@@ -592,7 +592,8 @@ module.exports = {
592
592
  getMissingAttachmentUrl() {
593
593
  const defaultIconUrl = '/modules/@apostrophecms/attachment/img/missing-icon.svg';
594
594
  self.apos.util.warn('Template warning: Impossible to retrieve the attachment url since it is missing, a default icon has been set. Please fix this ASAP!');
595
- return defaultIconUrl;
595
+ // Convert static asset path to full URL, which matters when static assets are in uploadfs
596
+ return self.apos.asset.url(defaultIconUrl);
596
597
  },
597
598
  // This method is available as a template helper: apos.attachment.url
598
599
  //
@@ -0,0 +1,14 @@
1
+ <?xml version="1.0" encoding="iso-8859-1"?>
2
+ <svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
3
+ viewBox="0 0 29.536 29.536" style="enable-background:new 0 0 29.536 29.536; fill: red;" xml:space="preserve">
4
+ <g>
5
+ <path d="M14.768,0C6.611,0,0,6.609,0,14.768c0,8.155,6.611,14.767,14.768,14.767s14.768-6.612,14.768-14.767
6
+ C29.535,6.609,22.924,0,14.768,0z M14.768,27.126c-6.828,0-12.361-5.532-12.361-12.359c0-6.828,5.533-12.362,12.361-12.362
7
+ c6.826,0,12.359,5.535,12.359,12.362C27.127,21.594,21.594,27.126,14.768,27.126z"/>
8
+ <path d="M14.385,19.337c-1.338,0-2.289,0.951-2.289,2.34c0,1.336,0.926,2.339,2.289,2.339c1.414,0,2.314-1.003,2.314-2.339
9
+ C16.672,20.288,15.771,19.337,14.385,19.337z"/>
10
+ <path d="M14.742,6.092c-1.824,0-3.34,0.513-4.293,1.053l0.875,2.804c0.668-0.462,1.697-0.772,2.545-0.772
11
+ c1.285,0.027,1.879,0.644,1.879,1.543c0,0.85-0.67,1.697-1.494,2.701c-1.156,1.364-1.594,2.701-1.516,4.012l0.025,0.669h3.42
12
+ v-0.463c-0.025-1.158,0.387-2.162,1.311-3.215c0.979-1.08,2.211-2.366,2.211-4.321C19.705,7.968,18.139,6.092,14.742,6.092z"/>
13
+ </g>
14
+ </svg>
@@ -7,7 +7,7 @@
7
7
  class="apos-button__icon"
8
8
  fill-color="color"
9
9
  />
10
- <slot name="label" v-if="label">
10
+ <slot v-if="label" name="label">
11
11
  {{ $t(label ) }}
12
12
  </slot>
13
13
  </span>
@@ -1047,6 +1047,7 @@ module.exports = {
1047
1047
  locale: toLocale,
1048
1048
  mode: 'draft'
1049
1049
  });
1050
+
1050
1051
  const toId = draft._id.replace(`:${draft.aposLocale}`, `:${toLocale}:draft`);
1051
1052
  const actionModule = self.apos.page.isPage(draft) ? self.apos.page : self;
1052
1053
  // Use findForEditing so that we are successful even for edge cases
@@ -1059,17 +1060,26 @@ module.exports = {
1059
1060
  const existing = await actionModule.findForEditing(toReq, {
1060
1061
  _id: toId
1061
1062
  }).permission('view').toObject();
1063
+
1064
+ const eventOptions = {
1065
+ source: draft.aposLocale.split(':')[0],
1066
+ target: toLocale,
1067
+ existing: Boolean(existing)
1068
+ };
1069
+
1062
1070
  // We only want to copy schema properties, leave non-schema
1063
1071
  // properties of the source document alone
1064
- const data = Object.fromEntries(Object.entries(draft).filter(([ key, value ]) => self.schema.find(field => field.name === key)));
1072
+ const data = Object.fromEntries(Object.entries(draft)
1073
+ .filter(
1074
+ ([ key, value ]) => key === 'type' || self.schema.find(field => field.name === key)
1075
+ ));
1065
1076
  // We need a slug even if removed from the schema for editing purposes
1066
1077
  data.slug = draft.slug;
1067
1078
  let result;
1068
1079
  if (!existing) {
1069
1080
  if (self.apos.page.isPage(draft)) {
1070
1081
  if (!draft.level) {
1071
- // Replicating the home page for the first time
1072
- result = await self.apos.doc.insert(toReq, {
1082
+ const insert = {
1073
1083
  ...data,
1074
1084
  aposDocId: draft.aposDocId,
1075
1085
  aposLocale: `${toLocale}:draft`,
@@ -1079,7 +1089,10 @@ module.exports = {
1079
1089
  rank: draft.rank,
1080
1090
  parked: draft.parked,
1081
1091
  parkedId: draft.parkedId
1082
- });
1092
+ };
1093
+ await self.emit('beforeLocalize', req, insert, eventOptions);
1094
+ // Replicating the home page for the first time
1095
+ result = await self.apos.doc.insert(toReq, insert);
1083
1096
  } else {
1084
1097
  // A page that is not the home page, being replicated for the first time
1085
1098
  let { lastTargetId, lastPosition } = await self.apos.page.inferLastTargetIdAndPosition(draft);
@@ -1137,25 +1150,29 @@ module.exports = {
1137
1150
  lastPosition = 'lastChild';
1138
1151
  }
1139
1152
  }
1153
+ const insert = {
1154
+ ...data,
1155
+ aposLocale: `${toLocale}:draft`,
1156
+ _id: toId,
1157
+ parked: draft.parked,
1158
+ parkedId: draft.parkedId
1159
+ };
1160
+ await self.emit('beforeLocalize', req, insert, eventOptions);
1140
1161
  result = await actionModule.insert(toReq,
1141
1162
  localizedTargetId,
1142
1163
  lastPosition,
1143
- {
1144
- ...data,
1145
- aposLocale: `${toLocale}:draft`,
1146
- _id: toId,
1147
- parked: draft.parked,
1148
- parkedId: draft.parkedId
1149
- }
1164
+ insert
1150
1165
  );
1151
1166
  }
1152
1167
  } else {
1153
- result = await actionModule.insert(toReq, {
1168
+ const insert = {
1154
1169
  ...data,
1155
1170
  aposDocId: draft.aposDocId,
1156
1171
  aposLocale: `${toLocale}:draft`,
1157
1172
  _id: toId
1158
- });
1173
+ };
1174
+ await self.emit('beforeLocalize', req, insert, eventOptions);
1175
+ result = await actionModule.insert(toReq, insert);
1159
1176
  }
1160
1177
  } else {
1161
1178
  if (!options.update) {
@@ -1169,8 +1186,12 @@ module.exports = {
1169
1186
  aposLocale: `${toLocale}:draft`,
1170
1187
  metaType: 'doc'
1171
1188
  };
1189
+ await self.emit('beforeLocalize', req, update, eventOptions);
1172
1190
  result = await actionModule.update(toReq, update);
1173
1191
  }
1192
+
1193
+ await self.emit('afterLocalize', req, draft, result, eventOptions);
1194
+
1174
1195
  return result;
1175
1196
  },
1176
1197
  // Reverts the given draft to the most recent publication.
@@ -4,9 +4,6 @@
4
4
  :menu="menu"
5
5
  :disabled="disabled || (menu.length === 0)"
6
6
  menu-placement="bottom-end"
7
- @item-clicked="menuHandler"
8
- @open="$emit('menu-open')"
9
- @close="$emit('menu-close')"
10
7
  :button="{
11
8
  tooltip: { content: 'apostrophe:moreOptions', placement: 'bottom' },
12
9
  label: 'apostrophe:moreOptions',
@@ -15,6 +12,9 @@
15
12
  type: 'subtle',
16
13
  modifiers: ['small', 'no-motion']
17
14
  }"
15
+ @item-clicked="menuHandler"
16
+ @open="$emit('menu-open')"
17
+ @close="$emit('menu-close')"
18
18
  />
19
19
  </template>
20
20
 
@@ -45,6 +45,15 @@
45
45
  "assetWebpackCacheCleared": "Build cache cleared.",
46
46
  "assetWebpackConfigWarning": "⚠️ In the module {{ module }}, your webpack config is incorrect. It must be an object and should contain only the properties: {{ properties }}.",
47
47
  "at": "at",
48
+ "automaticTranslationCheckbox": "Translate text content",
49
+ "automaticTranslationErrMsg": "An error happened while getting available languages for translation. Translation will be skipped.",
50
+ "automaticTranslationErrorNoProvider": "Translation provider not found. Page \"{{ title }}\" was not translated.",
51
+ "automaticTranslationErrorTargets": "Badly formatted translation targets.",
52
+ "automaticTranslationLngCheckNoProvider": "Translation provider not found.",
53
+ "automaticTranslationSettings": "Automatic translation settings",
54
+ "automaticTranslationSourceErrMsg": "The current locale <strong>{{ source }}</strong> is not suitable for translation. Translation will be skipped.",
55
+ "automaticTranslationTargetErrMsg": "Could not find a suitable language for the locale <strong>{{ targets }}</strong>. Translation will be skipped for this locale.",
56
+ "automaticTranslationTargetErrMsg_plural": "Could not find a suitable language for the locales <strong>{{ targets }}</strong>. Translation will be skipped for these locales.",
48
57
  "back": "Back",
49
58
  "backToHome": "Back to Home",
50
59
  "basics": "Basics",
@@ -149,6 +158,8 @@
149
158
  "email": "Email",
150
159
  "emptyRichTextWidget": "Empty Rich Text Widget",
151
160
  "enabled": "Enabled",
161
+ "enterPassword": "Enter password",
162
+ "enterUsername": "Enter username",
152
163
  "error": "An error occurred",
153
164
  "errorBatchOperationNoti": "Batch operation {{ operation }} failed.",
154
165
  "errorCount": "{{ count }} error remaining",
@@ -281,6 +292,7 @@
281
292
  "noNewRelatedDocuments": "Although this document has related documents, none of them are new to the locales you have selected.",
282
293
  "noTypeFound": "No {{ type }} Found",
283
294
  "none": "None",
295
+ "notAvailable": "n/a",
284
296
  "notFound": "Not found.",
285
297
  "notFoundPageMessage": "We can't seem to find the page you're looking for.",
286
298
  "notFoundPageStatusCode": "404",
@@ -366,6 +378,7 @@
366
378
  "restoredPrevious": "Restored previously published version.",
367
379
  "resumeEditing": "Resume Editing",
368
380
  "retryingSaveDocument": "Retrying save document...",
381
+ "retry": "Retry",
369
382
  "returnToPage": "Return to {{ label }}",
370
383
  "richText": "Rich Text",
371
384
  "richTextAlignCenter": "Align Center",
@@ -215,6 +215,46 @@
215
215
  {{ $t('apostrophe:noNewRelatedDocuments') }}
216
216
  </p>
217
217
  </div>
218
+ <div v-if="translationEnabled" class="apos-wizard__translation">
219
+ <p class="apos-wizard__translation-title">
220
+ <AposTranslationIndicator :size="18" />
221
+ <span class="apos-wizard__translation-title-text">
222
+ {{ $t('apostrophe:automaticTranslationSettings') }}
223
+ </span>
224
+ </p>
225
+ <AposCheckbox
226
+ v-model="wizard.values.translateContent.data"
227
+ :field="{ name: 'translate' }"
228
+ :choice="{
229
+ value: wizard.values.translateContent.data,
230
+ label: $t('apostrophe:automaticTranslationCheckbox')
231
+ }"
232
+ data-apos-test="localizationTranslationCheck"
233
+ />
234
+
235
+ <div v-if="translationErrMsg">
236
+ <!-- eslint-disable vue/no-v-html -->
237
+ <p
238
+ class="apos-wizard__translation-error"
239
+ data-apos-test="localizationTranslationErr"
240
+ v-html="translationErrMsg"
241
+ />
242
+ <!-- eslint-disable vue/no-v-html -->
243
+ <AposButton
244
+ v-if="translationShowRetry"
245
+ label="apostrophe:retry"
246
+ :modifiers="['quiet', 'no-motion']"
247
+ data-apos-test="localizationTranslationRetry"
248
+ @click="retryTranslationCheck()"
249
+ />
250
+ </div>
251
+ <div
252
+ v-else-if="translationShowLoader"
253
+ class="apos-wizard__translation-spinner"
254
+ >
255
+ <AposSpinner />
256
+ </div>
257
+ </div>
218
258
  </fieldset>
219
259
  </form>
220
260
  </template>
@@ -338,7 +378,10 @@ export default {
338
378
  toLocalize: { data: 'thisDocAndRelated' },
339
379
  toLocales: { data: this.locale ? [ this.locale ] : [] },
340
380
  relatedDocSettings: { data: 'localizeNewRelated' },
341
- relatedDocTypesToLocalize: { data: [] }
381
+ relatedDocTypesToLocalize: { data: [] },
382
+ translateContent: { data: false },
383
+ translateTargets: { data: [] },
384
+ translateProvider: { data: apos.translation.providers[0]?.name || null }
342
385
  }
343
386
  },
344
387
  fullDoc: this.doc,
@@ -369,7 +412,11 @@ export default {
369
412
  value: 'relatedDocsOnly',
370
413
  label: 'apostrophe:relatedDocsOnly'
371
414
  }
372
- ]
415
+ ],
416
+ translationEnabled: apos.modules['@apostrophecms/translation'].enabled,
417
+ translationErrMsg: null,
418
+ translationShowRetry: false,
419
+ translationShowLoader: false
373
420
  };
374
421
  },
375
422
  computed: {
@@ -428,7 +475,7 @@ export default {
428
475
  },
429
476
  visibleSections() {
430
477
  const self = this;
431
- const result = Object.entries(this.wizard.sections).filter(([ name, section ]) => {
478
+ const result = Object.entries(this.wizard.sections).filter(([ _, section ]) => {
432
479
  return section.if ? section.if.bind(self)() : true;
433
480
  }).map(([ name, section ]) => {
434
481
  return {
@@ -463,6 +510,9 @@ export default {
463
510
  'wizard.values.toLocalize.data'() {
464
511
  this.updateRelatedDocs();
465
512
  },
513
+ async 'wizard.values.translateContent.data'(value) {
514
+ await this.checkAvailableTranslations(value);
515
+ },
466
516
  selectedLocales() {
467
517
  this.updateRelatedDocs();
468
518
  },
@@ -516,6 +566,13 @@ export default {
516
566
  },
517
567
  goToPrevious() {
518
568
  this.wizard.step = this.previousStepName;
569
+ this.uncheckTranslate();
570
+ },
571
+ uncheckTranslate() {
572
+ this.wizard.values.translateContent.data = false;
573
+ this.wizard.values.translateTargets.data = [];
574
+ this.translationErrMsg = null;
575
+ this.translationShowRetry = false;
519
576
  },
520
577
  goToNext() {
521
578
  this.goTo(this.nextStepName);
@@ -661,6 +718,10 @@ export default {
661
718
  toLocale: locale.name,
662
719
  update: (doc._id === this.fullDoc._id) || !(this.wizard.values.relatedDocSettings.data === 'localizeNewRelated')
663
720
  },
721
+ qs: {
722
+ aposTranslateTargets: this.wizard.values.translateTargets.data,
723
+ aposTranslateProvider: this.wizard.values.translateProvider.data
724
+ },
664
725
  busy: true
665
726
  });
666
727
 
@@ -797,7 +858,11 @@ export default {
797
858
  // never be considered "related" to other pages simply because
798
859
  // of navigation links, the feature is meant for pieces that feel more like
799
860
  // part of the document being localized)
800
- return related.filter(doc => apos.modules[doc.type].relatedDocument !== false);
861
+ // We also remove non localized content like users
862
+ return related.filter(doc => {
863
+ return apos.modules[doc.type].relatedDocument !== false &&
864
+ apos.modules[doc.type].localized !== false;
865
+ });
801
866
  }
802
867
  },
803
868
  async updateRelatedDocs() {
@@ -831,6 +896,77 @@ export default {
831
896
  }
832
897
  this.relatedDocs = relatedDocs;
833
898
  this.wizard.busy = status;
899
+ },
900
+ wait(time) {
901
+ return new Promise((resolve) => {
902
+ setTimeout(() => {
903
+ resolve();
904
+ }, time);
905
+ });
906
+ },
907
+ async retryTranslationCheck() {
908
+ await this.checkAvailableTranslations(false);
909
+ this.translationShowLoader = true;
910
+ await this.wait(500);
911
+ await this.checkAvailableTranslations(true);
912
+ this.translationShowLoader = false;
913
+ },
914
+ async checkAvailableTranslations(value) {
915
+ if (!value) {
916
+ this.translationErrMsg = null;
917
+ this.translationShowRetry = false;
918
+ this.wizard.values.translateTargets.data = [];
919
+ return;
920
+ }
921
+ const [ sourceLocale ] = this.doc.aposLocale.split(':');
922
+ const targets = this.wizard.values.toLocales.data;
923
+
924
+ let response;
925
+ try {
926
+ response = await apos.http.get(`${apos.translation.action}/languages`, {
927
+ qs: {
928
+ provider: this.wizard.values.translateProvider.data,
929
+ source: [ sourceLocale ],
930
+ target: targets.map(({ name }) => name)
931
+ }
932
+ });
933
+ } catch (err) {
934
+ console.error('An error happened while getting available languages: ', err);
935
+ this.wizard.values.translateTargets.data = [];
936
+ this.translationErrMsg = this.$t('apostrophe:automaticTranslationErrMsg');
937
+ this.translationShowRetry = true;
938
+ return;
939
+ }
940
+
941
+ const unavailableSource = !response.source[0].supported;
942
+ const unavailableTargetsLabels = response.target
943
+ .filter(({ supported }) => !supported)
944
+ .map(({ code }) => targets.find((locale) => locale.name === code)?.label || code);
945
+
946
+ if (unavailableSource) {
947
+ const sourceLabel = this.moduleOptions.locales[sourceLocale]?.label;
948
+ this.translationErrMsg = this.$t('apostrophe:automaticTranslationSourceErrMsg', { source: sourceLabel });
949
+ this.wizard.values.translateTargets.data = [];
950
+ return;
951
+ }
952
+
953
+ if (unavailableTargetsLabels.length) {
954
+ const isPlural = unavailableTargetsLabels.length > 1;
955
+ this.translationErrMsg = this.$t(
956
+ `apostrophe:automaticTranslationTargetErrMsg${isPlural ? '_plural' : ''}`,
957
+ { targets: unavailableTargetsLabels.join(', ') }
958
+ );
959
+ }
960
+
961
+ if (unavailableTargetsLabels.length >= targets.length) {
962
+ this.wizard.values.translateTargets.data = [];
963
+ return;
964
+ }
965
+
966
+ this.wizard.values.translateTargets.data = response.target
967
+ .filter(({ supported }) => supported)
968
+ .map(({ code }) => code);
969
+
834
970
  }
835
971
  }
836
972
  };
@@ -1020,9 +1156,12 @@ export default {
1020
1156
  }
1021
1157
  }
1022
1158
 
1159
+ .apos-wizard__step :deep(.apos-field--relatedDocTypesToLocalize) {
1160
+ margin-top: $spacing-triple;
1161
+ }
1023
1162
  .apos-wizard__step {
1024
- .apos-field__wrapper:not(:last-of-type) {
1025
- margin-bottom: $spacing-triple;
1163
+ .apos-field__wrapper {
1164
+ margin-bottom: $spacing-double;
1026
1165
  }
1027
1166
  }
1028
1167
 
@@ -1065,4 +1204,32 @@ export default {
1065
1204
  .apos-locale-name {
1066
1205
  text-transform: uppercase;
1067
1206
  }
1207
+
1208
+ .apos-wizard__translation {
1209
+ margin-top: 30px;
1210
+ }
1211
+
1212
+ .apos-wizard__translation-title {
1213
+ @include type-label;
1214
+
1215
+ display: flex;
1216
+ align-items: center;
1217
+ border-bottom: 1px solid var(--a-base-8);
1218
+ padding-bottom: 8px;
1219
+ }
1220
+
1221
+ .apos-wizard__translation-title-text {
1222
+ margin-left: 7px;
1223
+ }
1224
+
1225
+ .apos-wizard__translation-error {
1226
+ @include type-label;
1227
+ color: var(--a-danger);
1228
+ }
1229
+
1230
+ .apos-wizard__translation-spinner {
1231
+ margin-top: 20px;
1232
+ display: flex;
1233
+ justify-content: center;
1234
+ }
1068
1235
  </style>
@@ -6,14 +6,15 @@
6
6
  <AposButton
7
7
  label="apostrophe:clear"
8
8
  type="quiet"
9
- @click="clear"
10
9
  :modifiers="['no-motion']"
10
+ @click="clear"
11
11
  />
12
12
  </div>
13
13
  <ol class="apos-media-manager-selections__items">
14
14
  <li
15
15
  v-for="item in items"
16
- :key="item._id" class="apos-media-manager-selections__item"
16
+ :key="item._id"
17
+ class="apos-media-manager-selections__item"
17
18
  >
18
19
  <div
19
20
  v-if="item.attachment && item.attachment._urls"
@@ -51,6 +51,10 @@ module.exports = {
51
51
  cascades: [ 'requirements' ],
52
52
  options: {
53
53
  alias: 'login',
54
+ placeholder: {
55
+ username: 'apostrophe:enterUsername',
56
+ password: 'apostrophe:enterPassword'
57
+ },
54
58
  localLogin: true,
55
59
  passwordReset: false,
56
60
  passwordResetHours: 48,
@@ -539,6 +543,7 @@ module.exports = {
539
543
 
540
544
  getBrowserData(req) {
541
545
  return {
546
+ schema: self.getSchema(),
542
547
  action: self.action,
543
548
  passwordResetEnabled: self.isPasswordResetEnabled(),
544
549
  ...(req.user ? {
@@ -909,8 +914,20 @@ module.exports = {
909
914
  last: true
910
915
  }
911
916
  );
912
- }
917
+ },
913
918
 
919
+ getSchema() {
920
+ return self.apos.user.schema
921
+ .filter(({ name }) => [ 'username', 'password' ].includes(name))
922
+ .map(field => ({
923
+ name: field.name,
924
+ label: field.label,
925
+ placeholder: self.options.placeholder[field.name],
926
+ type: field.type,
927
+ required: true
928
+ })
929
+ );
930
+ }
914
931
  };
915
932
  },
916
933
 
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <div
3
- class="apos-login-form"
4
3
  v-if="passwordResetEnabled && ready"
4
+ class="apos-login-form"
5
5
  >
6
6
  <TheAposLoginHeader
7
7
  :env="context.env"
@@ -27,8 +27,8 @@
27
27
  @submit.prevent="submit"
28
28
  >
29
29
  <AposSchema
30
- :schema="schema"
31
30
  v-model="doc"
31
+ :schema="schema"
32
32
  />
33
33
  <AposButton
34
34
  data-apos-test="pwdResetSubmit"