apostrophe 3.52.0 → 3.53.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 (52) hide show
  1. package/CHANGELOG.md +60 -2
  2. package/defaults.js +1 -0
  3. package/index.js +3 -2
  4. package/lib/check-if-conditions.js +44 -0
  5. package/lib/moog-require.js +23 -1
  6. package/modules/@apostrophecms/admin-bar/index.js +30 -1
  7. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +4 -1
  8. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextTitle.vue +14 -8
  9. package/modules/@apostrophecms/area/ui/apos/components/AposAreaExpandedMenu.vue +8 -2
  10. package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +4 -0
  11. package/modules/@apostrophecms/command-menu/ui/apos/components/AposCommandMenuShortcut.vue +1 -0
  12. package/modules/@apostrophecms/doc/index.js +13 -7
  13. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocContextMenu.vue +36 -22
  14. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +35 -27
  15. package/modules/@apostrophecms/i18n/i18n/en.json +8 -0
  16. package/modules/@apostrophecms/i18n/index.js +49 -2
  17. package/modules/@apostrophecms/image/ui/apos/components/AposMediaUploader.vue +16 -1
  18. package/modules/@apostrophecms/login/index.js +5 -1
  19. package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +2 -0
  20. package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +37 -40
  21. package/modules/@apostrophecms/modal/ui/apos/components/AposModalConfirm.vue +1 -2
  22. package/modules/@apostrophecms/modal/ui/apos/components/AposModalShareDraft.vue +3 -2
  23. package/modules/@apostrophecms/modal/ui/apos/components/AposModalTabs.vue +4 -5
  24. package/modules/@apostrophecms/modal/ui/apos/mixins/AposFocusMixin.js +91 -0
  25. package/modules/@apostrophecms/modal/ui/apos/mixins/AposModalTabsMixin.js +16 -4
  26. package/modules/@apostrophecms/page/ui/apos/components/AposPagesManager.vue +9 -3
  27. package/modules/@apostrophecms/piece-type/index.js +1 -1
  28. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +2 -0
  29. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +1 -1
  30. package/modules/@apostrophecms/schema/index.js +13 -0
  31. package/modules/@apostrophecms/schema/lib/addFieldTypes.js +3 -10
  32. package/modules/@apostrophecms/schema/ui/apos/components/AposArrayEditor.vue +0 -1
  33. package/modules/@apostrophecms/schema/ui/apos/components/AposInputSlug.vue +1 -15
  34. package/modules/@apostrophecms/schema/ui/apos/components/AposSchema.vue +20 -13
  35. package/modules/@apostrophecms/schema/ui/apos/components/AposSubform.vue +164 -0
  36. package/modules/@apostrophecms/schema/ui/apos/logic/AposSubform.js +141 -0
  37. package/modules/@apostrophecms/settings/index.js +627 -0
  38. package/modules/@apostrophecms/settings/ui/apos/apps/TheAposSettings.js +8 -0
  39. package/modules/@apostrophecms/settings/ui/apos/components/AposSettingsManager.vue +162 -0
  40. package/modules/@apostrophecms/settings/ui/apos/logic/AposSettingsManager.js +169 -0
  41. package/modules/@apostrophecms/ui/ui/apos/components/AposButton.vue +10 -0
  42. package/modules/@apostrophecms/ui/ui/apos/components/AposButtonSplit.vue +23 -6
  43. package/modules/@apostrophecms/ui/ui/apos/components/AposCellLabels.vue +1 -7
  44. package/modules/@apostrophecms/ui/ui/apos/components/AposSubformPreview.vue +136 -0
  45. package/modules/@apostrophecms/ui/ui/apos/lib/i18next.js +6 -6
  46. package/modules/@apostrophecms/ui/ui/apos/mixins/AposCellMixin.js +9 -0
  47. package/modules/@apostrophecms/ui/ui/apos/scss/global/_admin.scss +9 -0
  48. package/modules/@apostrophecms/ui/ui/apos/scss/global/_widgets.scss +5 -1
  49. package/modules/@apostrophecms/user/index.js +30 -3
  50. package/package.json +1 -1
  51. package/test/i18n.js +168 -0
  52. package/test/settings.js +544 -0
@@ -303,8 +303,13 @@
303
303
  "pages": "Pages",
304
304
  "parentNotLocalized": "Localize the parent page first",
305
305
  "password": "Password",
306
+ "passwordChangeHelp": "Modify your existing password",
307
+ "passwordCurrent": "Current Password",
308
+ "passwordCurrentError": "Current password is incorrect",
306
309
  "passwordErrorMax": "Maximum of {{ max }} characters",
307
310
  "passwordErrorMin": "Minimum of {{ min }} characters",
311
+ "passwordNew": "New Password",
312
+ "passwordRepeat": "Repeat New Password",
308
313
  "passwordResetRequest": "Your request to reset your password on {{ site }}",
309
314
  "pasteWidget": "Paste {{ widget }}",
310
315
  "pending": "Pending",
@@ -420,6 +425,7 @@
420
425
  "selectPage": "Select Page",
421
426
  "selectedMenuItem": "✓ {{ label }}",
422
427
  "sentenceJoiner": " ",
428
+ "settings": "Personal Settings",
423
429
  "shareDraft": "Share Draft",
424
430
  "shareDraftCopyLink": "Copy draft link",
425
431
  "shareDraftDescription": "Enabling draft sharing will allow anyone with this link to view the current draft",
@@ -464,6 +470,8 @@
464
470
  "type": "Type",
465
471
  "typeWithCount": "{{ type }} ({{ count }})",
466
472
  "unableToSwitchModes": "Unable to switch modes.",
473
+ "uiLanguageLabel": "UI Language",
474
+ "uiLanguageWebsite": "Same as Website",
467
475
  "undo": "Undo",
468
476
  "undoFailed": "The operation could not be undone.",
469
477
  "undoPublish": "Undo Publish",
@@ -4,6 +4,34 @@
4
4
  //
5
5
  // `apos.i18n.i18next` can be used to directly access the `i18next` npm module instance if necessary.
6
6
  // It usually is not necessary. Use `req.t` if you need to localize in a route.
7
+ //
8
+ // ## Options
9
+ //
10
+ // ### `locales` TODO
11
+ //
12
+ // ### `defaultLocale` TODO
13
+ //
14
+ // ### `adminLocales`
15
+ //
16
+ // Controls what admin UI language can be set per user. If set, `adminLocale` user field
17
+ // will be automatically added to the user schema.
18
+ // Contains an array of objects with `label` and `value` properties:
19
+ // ```js
20
+ // {
21
+ // label: 'English',
22
+ // value: 'en'
23
+ // }
24
+ // ```
25
+ //
26
+ // ### `defaultAdminLocale`
27
+ //
28
+ // The default admin UI language. If `adminLocales` are configured, it should
29
+ // should match a `value` property from the list. Furthermore, it will be used
30
+ // as the default value for the`adminLocale` user field. If it is not set,
31
+ // but `adminLocales` is set, then the default is to display the admin UI
32
+ // in the same language as the website content.
33
+ // Example: `defaultLocale: 'fr'`.
34
+ //
7
35
 
8
36
  const i18next = require('i18next');
9
37
  const fs = require('fs');
@@ -55,6 +83,12 @@ module.exports = {
55
83
  self.locales = self.getLocales();
56
84
  self.hostnamesInUse = Object.values(self.locales).find(locale => locale.hostname);
57
85
  self.defaultLocale = self.options.defaultLocale || Object.keys(self.locales)[0];
86
+ // Contains label/value object for each locale
87
+ self.adminLocales = self.options.adminLocales || [];
88
+ // Contains only the string value of the default admin locale (e.g. 'en').
89
+ // If adminLocales are configured, it should be one of them. Otherwise,
90
+ // it can be any valid locale string identifier.
91
+ self.defaultAdminLocale = self.options.defaultAdminLocale || null;
58
92
  // Lint the locale configurations
59
93
  for (const [ key, options ] of Object.entries(self.locales)) {
60
94
  if (!options) {
@@ -70,6 +104,15 @@ module.exports = {
70
104
  throw self.apos.error('invalid', `Locale prefixes must not contain more than one forward slash ("/").\nUse hyphens as separators. Check locale "${key}".`);
71
105
  }
72
106
  }
107
+ if (!Array.isArray(self.adminLocales)) {
108
+ throw self.apos.error('invalid', 'The "adminLocales" option must be an array.');
109
+ }
110
+ if (self.defaultAdminLocale && typeof self.defaultAdminLocale !== 'string') {
111
+ throw self.apos.error('invalid', 'The "defaultAdminLocale" option must be a string.');
112
+ }
113
+ if (self.defaultAdminLocale && self.adminLocales.length && !self.adminLocales.some(al => al.value === self.defaultAdminLocale)) {
114
+ throw self.apos.error('invalid', `The value of "defaultAdminLocale" "${self.defaultAdminLocale}" doesn't match any of the existing "adminLocales" values.`);
115
+ }
73
116
  const fallbackLng = [ self.defaultLocale ];
74
117
  // In case the default locale also has inadequate admin UI phrases
75
118
  if (fallbackLng[0] !== 'en') {
@@ -574,10 +617,13 @@ module.exports = {
574
617
  }
575
618
  },
576
619
  getBrowserData(req) {
620
+ const adminLocale = req.user?.adminLocale === ''
621
+ ? req.locale
622
+ : req.user?.adminLocale || self.defaultAdminLocale || req.locale;
577
623
  const i18n = {
578
- [req.locale]: self.getBrowserBundles(req.locale)
624
+ [adminLocale]: self.getBrowserBundles(adminLocale)
579
625
  };
580
- if (req.locale !== self.defaultLocale) {
626
+ if (adminLocale !== self.defaultLocale) {
581
627
  i18n[self.defaultLocale] = self.getBrowserBundles(self.defaultLocale);
582
628
  }
583
629
  // In case the default locale also has inadequate admin UI phrases
@@ -587,6 +633,7 @@ module.exports = {
587
633
  const result = {
588
634
  i18n,
589
635
  locale: req.locale,
636
+ adminLocale,
590
637
  defaultLocale: self.defaultLocale,
591
638
  defaultNamespace: self.defaultNamespace,
592
639
  locales: self.locales,
@@ -7,7 +7,11 @@
7
7
  @dragenter="incrementDragover"
8
8
  @dragleave="decrementDragover"
9
9
  >
10
- <div class="apos-media-uploader__inner">
10
+ <div
11
+ class="apos-media-uploader__inner"
12
+ tabindex="0"
13
+ @keydown="onUploadDragAndDropKeyDown"
14
+ >
11
15
  <AposCloudUploadIcon
12
16
  class="apos-media-uploader__icon"
13
17
  />
@@ -30,6 +34,7 @@
30
34
  :accept="accept"
31
35
  multiple="true"
32
36
  :disabled="disabled"
37
+ tabindex="-1"
33
38
  >
34
39
  </label>
35
40
  </template>
@@ -222,6 +227,16 @@ export default {
222
227
  });
223
228
  }
224
229
  }
230
+ },
231
+ // Trigger the file input click (via `this.create`) when pressing Enter or Space
232
+ // of the drag&drop area, which is made focusable unlike the input file.
233
+ onUploadDragAndDropKeyDown(e) {
234
+ const isEnterPressed = e.key === 'Enter' || e.code === 'Enter' || e.code === 'NumpadEnter';
235
+ const isSpaceBarPressed = e.keyCode === 32 || e.code === 'Space';
236
+
237
+ if (isEnterPressed || isSpaceBarPressed) {
238
+ this.create();
239
+ }
225
240
  }
226
241
  }
227
242
  };
@@ -892,7 +892,11 @@ module.exports = {
892
892
  if (err) {
893
893
  return callback(err);
894
894
  }
895
- await self.emit('afterSessionLogin', req);
895
+ try {
896
+ await self.emit('afterSessionLogin', req);
897
+ } catch (e) {
898
+ return callback(e);
899
+ }
896
900
  // Make sure no handler removed req.user
897
901
  if (req.user) {
898
902
  // Mark the login timestamp. Middleware takes care of ensuring
@@ -11,6 +11,7 @@
11
11
  :icon="checkboxIcon"
12
12
  @click="selectAll"
13
13
  ref="selectAll"
14
+ data-apos-test="selectAll"
14
15
  />
15
16
  <div
16
17
  v-for="{
@@ -25,6 +26,7 @@
25
26
  <AposButton
26
27
  v-if="!operations"
27
28
  :label="label"
29
+ :action="action"
28
30
  :icon="icon"
29
31
  :disabled="!checkedCount"
30
32
  :modifiers="['small']"
@@ -12,6 +12,8 @@
12
12
  aria-modal="true"
13
13
  :aria-labelledby="id"
14
14
  ref="modalEl"
15
+ @keydown="cycleElementsToFocus"
16
+ @focus.capture="storeFocusedElement"
15
17
  data-apos-modal
16
18
  >
17
19
  <transition :name="transitionType">
@@ -92,8 +94,13 @@
92
94
  // So as the modal exits, they should change in reverse. `showModal` becomes
93
95
  // `false`, then `active` is set to `false` once the modal has finished its
94
96
  // transition.
97
+ import AposFocusMixin from 'Modules/@apostrophecms/modal/mixins/AposFocusMixin';
98
+
95
99
  export default {
96
100
  name: 'AposModal',
101
+ mixins: [
102
+ AposFocusMixin
103
+ ],
97
104
  props: {
98
105
  modal: {
99
106
  type: Object,
@@ -123,8 +130,11 @@ export default {
123
130
  return 'fade';
124
131
  }
125
132
  },
126
- modalReady () {
127
- return this.modal.active;
133
+ shouldTrapFocus() {
134
+ return this.modal.trapFocus || this.modal.trapFocus === undefined;
135
+ },
136
+ triggerFocusRefresh () {
137
+ return this.modal.triggerFocusRefresh;
128
138
  },
129
139
  hasBeenLocalized: function() {
130
140
  return Object.keys(apos.i18n.locales).length > 1;
@@ -187,12 +197,19 @@ export default {
187
197
  }
188
198
  },
189
199
  watch: {
190
- modalReady (newVal) {
191
- this.$nextTick(() => {
192
- if (newVal && this.modal.trapFocus && this.$refs.modalEl) {
193
- this.trapFocus();
194
- }
195
- });
200
+ // Simple way to re-trigger focusable elements
201
+ // that might have been created or removed
202
+ // after an update, like an XHR call to get the
203
+ // pieces list in the AposDocsManager modal, for instance.
204
+ triggerFocusRefresh (newVal) {
205
+ if (this.shouldTrapFocus) {
206
+ this.$nextTick(this.trapFocus);
207
+ }
208
+ }
209
+ },
210
+ mounted() {
211
+ if (this.shouldTrapFocus) {
212
+ this.$nextTick(this.trapFocus);
196
213
  }
197
214
  },
198
215
  methods: {
@@ -217,6 +234,7 @@ export default {
217
234
  // pop doesn't quite suffice because of race conditions when
218
235
  // closing one and opening another
219
236
  apos.modal.stack = apos.modal.stack.filter(modal => modal !== this);
237
+ this.focusLastModalFocusedElement();
220
238
  },
221
239
  bindEventListeners () {
222
240
  window.addEventListener('keydown', this.onKeydown);
@@ -232,47 +250,26 @@ export default {
232
250
  this.$emit('esc');
233
251
  },
234
252
  trapFocus () {
235
- // Adapted from https://uxdesign.cc/how-to-trap-focus-inside-modal-to-make-it-ada-compliant-6a50f9a70700
236
- // All the elements inside modal which you want to make focusable.
237
- const focusableElements = [
238
- 'button',
253
+ const elementSelectors = [
254
+ '[tabindex]',
239
255
  '[href]',
240
256
  'input',
241
257
  'select',
242
258
  'textarea',
243
- '[tabindex]:not([tabindex="-1"]'
259
+ 'button'
244
260
  ];
245
- const focusableString = focusableElements.join(', ');
246
- const modalEl = this.$refs.modalEl;
247
- const focusables = modalEl.querySelectorAll(focusableString);
248
- const firstFocusableElement = focusables[0];
249
- const lastFocusableElement = focusables[focusables.length - 1];
250
261
 
251
- modalEl.addEventListener('keydown', cycleFocusables);
262
+ const selector = elementSelectors
263
+ .map(addExcludingAttributes)
264
+ .join(', ');
252
265
 
253
- firstFocusableElement.focus();
266
+ this.elementsToFocus = [ ...this.$refs.modalEl.querySelectorAll(selector) ]
267
+ .filter(this.isElementVisible);
254
268
 
255
- function cycleFocusables (e) {
256
- const isTabPressed = e.key === 'Tab' || e.code === 'Tab';
257
- if (!isTabPressed) {
258
- return;
259
- }
269
+ this.focusElement(this.focusedElement, this.elementsToFocus[0]);
260
270
 
261
- if (e.shiftKey) {
262
- // If shift key pressed for shift + tab combination
263
- if (document.activeElement === firstFocusableElement) {
264
- // Add focus for the last focusable element
265
- lastFocusableElement.focus();
266
- e.preventDefault();
267
- }
268
- } else {
269
- // If tab key is pressed
270
- if (document.activeElement === lastFocusableElement) {
271
- // Add focus for the first focusable element
272
- firstFocusableElement.focus();
273
- e.preventDefault();
274
- }
275
- }
271
+ function addExcludingAttributes(element) {
272
+ return `${element}:not([tabindex="-1"]):not([disabled]):not([type="hidden"]):not([aria-hidden])`;
276
273
  }
277
274
  }
278
275
  }
@@ -95,8 +95,7 @@ export default {
95
95
  active: false,
96
96
  type: 'overlay',
97
97
  showModal: false,
98
- disableHeader: true,
99
- trapFocus: true
98
+ disableHeader: true
100
99
  },
101
100
  formValues: null
102
101
  };
@@ -17,6 +17,7 @@
17
17
  </h2>
18
18
  <Close
19
19
  class="apos-share-draft__close"
20
+ tabindex="0"
20
21
  :title="$t('apostrophe:close')"
21
22
  :size="18"
22
23
  @click.prevent="close"
@@ -48,6 +49,7 @@
48
49
  v-model="shareUrl"
49
50
  type="text"
50
51
  disabled
52
+ tabindex="-1"
51
53
  class="apos-share-draft__url"
52
54
  >
53
55
  <a
@@ -93,8 +95,7 @@ export default {
93
95
  active: false,
94
96
  type: 'overlay',
95
97
  showModal: false,
96
- disableHeader: true,
97
- trapFocus: true
98
+ disableHeader: true
98
99
  },
99
100
  shareUrl: '',
100
101
  disabled: true
@@ -2,12 +2,14 @@
2
2
  <div class="apos-modal-tabs">
3
3
  <ul class="apos-modal-tabs__tabs">
4
4
  <li
5
- class="apos-modal-tabs__tab" v-for="tab in tabs"
5
+ class="apos-modal-tabs__tab"
6
+ v-for="tab in tabs"
6
7
  :key="tab.name"
8
+ v-show="tab.isVisible !== false"
7
9
  >
8
10
  <button
9
11
  :id="tab.name" class="apos-modal-tabs__btn"
10
- :aria-selected="tab.name === currentTab ? true : false"
12
+ :aria-selected="tab.name === current ? true : false"
11
13
  @click="selectTab"
12
14
  >
13
15
  {{ $t(tab.label) }}
@@ -41,9 +43,6 @@ export default {
41
43
  },
42
44
  emits: [ 'select-tab' ],
43
45
  computed: {
44
- currentTab() {
45
- return this.current || this.tabs[0].name;
46
- },
47
46
  tabErrors() {
48
47
  const errors = {};
49
48
  for (const key in this.errors) {
@@ -0,0 +1,91 @@
1
+ /*
2
+ * Provides:
3
+ *
4
+ * Methods to handle focus with keyboard.
5
+ */
6
+ export default {
7
+ data() {
8
+ return {
9
+ elementsToFocus: [],
10
+
11
+ // specific to modals:
12
+ focusedElement: null
13
+ };
14
+ },
15
+ methods: {
16
+ // Adapted from https://uxdesign.cc/how-to-trap-focus-inside-modal-to-make-it-ada-compliant-6a50f9a70700
17
+ // All the elements inside modal which you want to make focusable.
18
+ //
19
+ // This has been adapted to Vue logic with `this.elementsToFocus` array as a data
20
+ // so that any elements, not only from a modal but a menu for instance, can be focusable.
21
+ // `cycleElementsToFocus` listeners relies on this dynamic list which has the advantage of
22
+ // taking new or less elements to focus, after an update has happened inside a modal,
23
+ // like an XHR call to get the pieces list in the AposDocsManager modal, for instance.
24
+ cycleElementsToFocus(e) {
25
+ if (!this.elementsToFocus.length) {
26
+ return;
27
+ }
28
+
29
+ const isTabPressed = e.key === 'Tab' || e.code === 'Tab';
30
+ if (!isTabPressed) {
31
+ return;
32
+ }
33
+
34
+ const firstElementToFocus = this.elementsToFocus.at(0);
35
+ const lastElementToFocus = this.elementsToFocus.at(-1);
36
+
37
+ // If shift key pressed for shift + tab combination
38
+ if (e.shiftKey) {
39
+ if (document.activeElement === firstElementToFocus) {
40
+ // Add focus for the last focusable element
41
+ lastElementToFocus.focus();
42
+ e.preventDefault();
43
+ }
44
+ return;
45
+ }
46
+
47
+ // If tab key is pressed
48
+ if (document.activeElement === lastElementToFocus) {
49
+ // Add focus for the first focusable element
50
+ firstElementToFocus.focus();
51
+ e.preventDefault();
52
+ }
53
+ },
54
+ // Focus the last focused element from the last modal.
55
+ // If it is not focusable (not visible/not in the DOM),
56
+ // fallbacks to the first focusable element from the last modal.
57
+ focusLastModalFocusedElement() {
58
+ const lastModal = apos.modal.stack.at(-1);
59
+
60
+ if (!lastModal) {
61
+ return;
62
+ }
63
+
64
+ const { focusedElement, elementsToFocus } = lastModal;
65
+
66
+ this.focusElement(focusedElement, elementsToFocus[0]);
67
+ },
68
+ storeFocusedElement(e) {
69
+ this.focusedElement = e.target;
70
+ },
71
+ // Iterate through elements given in arguments and
72
+ // focus the first element that exists in the DOM.
73
+ focusElement(...elementsToFocus) {
74
+ for (const element of elementsToFocus) {
75
+ const isAlreadySelected = document.activeElement === element;
76
+
77
+ if (!element || !this.isElementVisible(element)) {
78
+ continue;
79
+ }
80
+ if (!isAlreadySelected) {
81
+ element.focus();
82
+ }
83
+ // Element exists in the DOM and is focused, stop iterating.
84
+ return;
85
+ }
86
+ },
87
+ isElementVisible(element) {
88
+ return element.offsetParent !== null;
89
+ }
90
+ }
91
+ };
@@ -46,28 +46,40 @@ export default {
46
46
  const tabs = [];
47
47
  for (const key in this.groups) {
48
48
  if (key !== 'utility') {
49
+ // AposRelationshipEditor does not implement AposEditorMixin with the function conditionalFields
50
+ const conditionalFields = this.conditionalFields?.('other') || [];
51
+ const fields = this.groups[key].fields;
49
52
  tabs.push({
50
53
  name: key,
51
54
  label: this.groups[key].label,
52
- fields: this.groups[key].fields
55
+ fields,
56
+ isVisible: fields.some(field => conditionalFields[field] !== false)
53
57
  });
54
58
  }
55
59
  }
56
60
 
57
61
  return tabs;
62
+ },
63
+ firstVisibleTabName() {
64
+ const { name = null } = this.tabs.find(tab => tab.isVisible === true) || this.tabs[0] || {};
65
+
66
+ return name;
58
67
  }
59
68
  },
60
69
 
61
70
  watch: {
62
71
  tabs() {
63
- if ((!this.currentTab) || (!this.tabs.find(tab => tab.name === this.currentTab))) {
64
- this.currentTab = this.tabs[0] && this.tabs[0].name;
72
+ if (
73
+ !this.currentTab ||
74
+ !this.tabs.some(tab => tab.isVisible === true && tab.name === this.currentTab)
75
+ ) {
76
+ this.currentTab = this.firstVisibleTabName;
65
77
  }
66
78
  }
67
79
  },
68
80
 
69
81
  mounted() {
70
- this.currentTab = this.tabs[0] ? this.tabs[0].name : null;
82
+ this.currentTab = this.firstVisibleTabName;
71
83
  },
72
84
  methods: {
73
85
  switchPane(id) {
@@ -1,8 +1,11 @@
1
1
  <template>
2
2
  <AposModal
3
- :modal="modal" modal-title="apostrophe:managePages"
4
- @esc="confirmAndCancel" @no-modal="$emit('safe-close')"
5
- @inactive="modal.active = false" @show-modal="modal.showModal = true"
3
+ :modal="modal"
4
+ modal-title="apostrophe:managePages"
5
+ @esc="confirmAndCancel"
6
+ @no-modal="$emit('safe-close')"
7
+ @inactive="modal.active = false"
8
+ @show-modal="modal.showModal = true"
6
9
  >
7
10
  <template #secondaryControls>
8
11
  <AposButton
@@ -99,6 +102,7 @@ export default {
99
102
  moduleName: '@apostrophecms/page',
100
103
  modal: {
101
104
  active: false,
105
+ triggerFocusRefresh: 0,
102
106
  type: 'slide',
103
107
  showModal: false,
104
108
  width: 'two-thirds'
@@ -221,6 +225,8 @@ export default {
221
225
  // Get the data. This will be more complex in actuality.
222
226
  this.modal.active = true;
223
227
  await this.getPages();
228
+ this.modal.triggerFocusRefresh++;
229
+
224
230
  apos.bus.$on('content-changed', this.getPages);
225
231
  apos.bus.$on('command-menu-manager-create-new', this.create);
226
232
  apos.bus.$on('command-menu-manager-close', this.confirmAndCancel);
@@ -179,7 +179,7 @@ module.exports = {
179
179
  label: 'apostrophe:restore',
180
180
  messages: {
181
181
  progress: 'Restoring {{ type }}...',
182
- completed: 'Restoring {{ count }} {{ type }}.'
182
+ completed: 'Restored {{ count }} {{ type }}.'
183
183
  },
184
184
  icon: 'archive-arrow-up-icon',
185
185
  if: {
@@ -137,6 +137,7 @@ export default {
137
137
  return {
138
138
  modal: {
139
139
  active: false,
140
+ triggerFocusRefresh: 0,
140
141
  type: 'overlay',
141
142
  showModal: false
142
143
  },
@@ -230,6 +231,7 @@ export default {
230
231
  this.modal.active = true;
231
232
  await this.getPieces();
232
233
  await this.getAllPiecesTotal();
234
+ this.modal.triggerFocusRefresh++;
233
235
 
234
236
  apos.bus.$on('content-changed', this.getPieces);
235
237
  apos.bus.$on('command-menu-manager-create-new', this.create);
@@ -596,7 +596,7 @@ export default {
596
596
  const { $to } = state.selection;
597
597
  if (state.selection.empty && $to?.nodeBefore?.text) {
598
598
  const text = $to.nodeBefore.text;
599
- if (text === '/') {
599
+ if (text.slice(-1) === '/') {
600
600
  const pos = this.editor.view.state.selection.$anchor.pos;
601
601
  // Select the slash so an insert operation can replace it
602
602
  this.editor.commands.setTextSelection({
@@ -1240,6 +1240,9 @@ module.exports = {
1240
1240
  if (!field.label && !field.contextual) {
1241
1241
  field.label = _.startCase(field.name.replace(/^_/, ''));
1242
1242
  }
1243
+ if (field.hidden && field.hidden !== true && field.hidden !== false) {
1244
+ fail(`hidden must be a boolean, "${field.hidden}" provided.`);
1245
+ }
1243
1246
  if (field.if && field.if.$or && !Array.isArray(field.if.$or)) {
1244
1247
  fail(`$or conditional must be an array of conditions. Current $or configuration: ${JSON.stringify(field.if.$or)}`);
1245
1248
  }
@@ -1631,6 +1634,16 @@ module.exports = {
1631
1634
  } catch (error) {
1632
1635
  throw self.apos.error('invalid', error.message);
1633
1636
  }
1637
+ },
1638
+
1639
+ getSlugFieldOptions(field, data) {
1640
+ const options = {
1641
+ def: field.def
1642
+ };
1643
+ if (field.page) {
1644
+ options.allow = '/';
1645
+ }
1646
+ return options;
1634
1647
  }
1635
1648
  };
1636
1649
  },
@@ -162,16 +162,9 @@ module.exports = (self) => {
162
162
  // if field.page is true, expect a page slug (slashes allowed,
163
163
  // leading slash required). Otherwise, expect a object-style slug
164
164
  // (no slashes at all)
165
- convert: function (req, field, data, destination) {
166
- const options = {
167
- def: field.def
168
- };
169
- if (field.page) {
170
- options.allow = '/';
171
- }
172
- if (data.aposIsTemplate) {
173
- options.allow = field.page ? [ '/', '@' ] : '@';
174
- }
165
+ convert (req, field, data, destination) {
166
+ const options = self.getSlugFieldOptions(field, data);
167
+
175
168
  destination[field.name] = self.apos.util.slugify(self.apos.launder.string(data[field.name], field.def), options);
176
169
 
177
170
  if (field.page) {
@@ -230,7 +230,6 @@ export default {
230
230
  aposSchema.scrollFieldIntoView(name);
231
231
  }
232
232
  this.titleFieldChoices = await this.getTitleFieldChoices();
233
-
234
233
  },
235
234
  methods: {
236
235
  async select(_id) {
@@ -191,9 +191,7 @@ export default {
191
191
  def: ''
192
192
  };
193
193
 
194
- if (this.field.aposIsTemplate) {
195
- options.allow = this.field.page ? [ '/', '@' ] : '@';
196
- } else if (this.field.page && !componentOnly) {
194
+ if (this.field.page && !componentOnly) {
197
195
  options.allow = '/';
198
196
  }
199
197
 
@@ -269,18 +267,6 @@ export default {
269
267
  // doc editor modal it will momentarily be tracked as archived but
270
268
  // without not have the archive prefix, so check that too.
271
269
  updated = this.isArchived && archivePrefix ? `${archivePrefix}${updated}` : updated;
272
- } else if (this.field.aposIsTemplate) {
273
- let prefix = '';
274
- if (this.field.page) {
275
- if (!updated.startsWith('/@')) {
276
- prefix = '/@';
277
- }
278
- } else {
279
- if (!updated.startsWith('@')) {
280
- prefix = '@';
281
- }
282
- }
283
- updated = prefix + updated;
284
270
  }
285
271
 
286
272
  return updated;