apostrophe 3.22.1 → 3.24.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 (40) hide show
  1. package/.github/workflows/main.yml +17 -17
  2. package/CHANGELOG.md +27 -0
  3. package/index.js +1 -1
  4. package/lib/locales.js +43 -0
  5. package/lib/moog-require.js +1 -1
  6. package/modules/@apostrophecms/doc-type/index.js +101 -10
  7. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocContextMenu.vue +11 -7
  8. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +8 -109
  9. package/modules/@apostrophecms/i18n/i18n/en.json +7 -1
  10. package/modules/@apostrophecms/i18n/i18n/es.json +7 -1
  11. package/modules/@apostrophecms/i18n/i18n/fr.json +7 -1
  12. package/modules/@apostrophecms/i18n/i18n/pt-BR.json +7 -1
  13. package/modules/@apostrophecms/i18n/i18n/sk.json +7 -1
  14. package/modules/@apostrophecms/i18n/index.js +16 -30
  15. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManager.vue +3 -1
  16. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerEditor.vue +37 -2
  17. package/modules/@apostrophecms/login/index.js +10 -0
  18. package/modules/@apostrophecms/modal/ui/apos/components/AposModalShareDraft.vue +323 -0
  19. package/modules/@apostrophecms/modal/ui/apos/mixins/AposDocErrorsMixin.js +79 -0
  20. package/modules/@apostrophecms/modal/ui/apos/mixins/AposEditorMixin.js +2 -1
  21. package/modules/@apostrophecms/modal/ui/apos/mixins/AposModalTabsMixin.js +58 -0
  22. package/modules/@apostrophecms/module/index.js +11 -0
  23. package/modules/@apostrophecms/page/index.js +39 -5
  24. package/modules/@apostrophecms/permission/index.js +26 -1
  25. package/modules/@apostrophecms/piece-type/index.js +22 -0
  26. package/modules/@apostrophecms/piece-type/ui/apos/components/AposRelationshipEditor.vue +53 -10
  27. package/modules/@apostrophecms/polymorphic-type/index.js +4 -1
  28. package/modules/@apostrophecms/schema/ui/apos/components/AposArrayEditor.vue +13 -3
  29. package/modules/@apostrophecms/soft-redirect/index.js +10 -1
  30. package/modules/@apostrophecms/task/index.js +3 -1
  31. package/modules/@apostrophecms/template/index.js +0 -1
  32. package/modules/@apostrophecms/ui/ui/apos/components/AposSlat.vue +3 -3
  33. package/package.json +2 -2
  34. package/test/caches.js +20 -0
  35. package/test/docs.js +4 -4
  36. package/test/modules/test-page/views/page.html +10 -5
  37. package/test/pages.js +147 -0
  38. package/test/pieces.js +151 -0
  39. package/test/static-i18n.js +14 -0
  40. package/test-lib/util.js +3 -2
@@ -11,6 +11,7 @@ const _ = require('lodash');
11
11
  const { stripIndent } = require('common-tags');
12
12
  const ExpressSessionCookie = require('express-session/session/cookie');
13
13
  const path = require('path');
14
+ const { verifyLocales } = require('../../../lib/locales');
14
15
 
15
16
  const apostropheI18nDebugPlugin = {
16
17
  type: 'postProcessor',
@@ -218,7 +219,8 @@ module.exports = {
218
219
  } else {
219
220
  locale = self.matchLocale(req);
220
221
  }
221
- const localeOptions = self.locales[locale];
222
+ const locales = self.filterPrivateLocales(req, self.locales);
223
+ const localeOptions = locales[locale];
222
224
  if (localeOptions.prefix) {
223
225
  // Remove locale prefix so URL parsing can proceed normally from here
224
226
  if (req.path === localeOptions.prefix) {
@@ -464,8 +466,9 @@ module.exports = {
464
466
  // possible the default locale is returned.
465
467
  matchLocale(req) {
466
468
  const hostname = req.hostname;
469
+ const locales = self.filterPrivateLocales(req, self.locales);
467
470
  let best = false;
468
- for (const [ name, options ] of Object.entries(self.locales)) {
471
+ for (const [ name, options ] of Object.entries(locales)) {
469
472
  const matchedHostname = options.hostname
470
473
  ? (hostname === options.hostname.split(':')[0]) : null;
471
474
  const matchedPrefix = options.prefix
@@ -568,34 +571,7 @@ module.exports = {
568
571
  label: 'English'
569
572
  }
570
573
  };
571
- const taken = {};
572
- let hostnamesCount = 0;
573
- for (const [ name, options ] of Object.entries(locales)) {
574
- const key = (options.hostname || '__none') + ':' + (options.prefix || '__none');
575
- hostnamesCount += (options.hostname ? 1 : 0);
576
- if (taken[key]) {
577
- throw new Error(stripIndent`
578
- @apostrophecms/i18n: the locale ${name} cannot be distinguished from
579
- earlier locales. Make sure it is uniquely distinguished by its hostname
580
- option, prefix option or a combination of the two. One locale per site
581
- may be a default with neither hostname nor prefix, and one locale per
582
- hostname may be a default for that hostname without a prefix.
583
- `);
584
- }
585
- taken[key] = true;
586
- }
587
- if ((hostnamesCount > 0) && (hostnamesCount < Object.keys(locales).length) && (!self.apos.options.baseUrl)) {
588
- throw new Error(stripIndent`
589
- If some of your locales have hostnames, then they all must have
590
- hostnames, or your top-level baseUrl option must be set.
591
-
592
- In development, you can set baseUrl to http://localhost:3000
593
- for testing purposes. In production it should always be set
594
- to a real base URL for the site.
595
- `);
596
- }
597
- // Make sure they are adequately distinguished by
598
- // hostname and prefix
574
+ verifyLocales(locales, self.apos.options.baseUrl);
599
575
  return locales;
600
576
  },
601
577
  sanitizeLocaleName(locale) {
@@ -656,6 +632,16 @@ module.exports = {
656
632
  }
657
633
  return res.redirect(corresponding._url);
658
634
  };
635
+ },
636
+ // Exclude private locales when logged out
637
+ filterPrivateLocales(req, locales) {
638
+ return req.user
639
+ ? locales
640
+ : Object.fromEntries(
641
+ Object
642
+ .entries(locales)
643
+ .filter(([ name, options ]) => options.private !== true)
644
+ );
659
645
  }
660
646
  };
661
647
  }
@@ -90,7 +90,9 @@
90
90
  >
91
91
  <AposMediaManagerEditor
92
92
  v-show="editing"
93
- :media="editing" :selected="selected"
93
+ :media="editing"
94
+ :selected="selected"
95
+ :is-modified="isModified"
94
96
  :module-labels="moduleLabels"
95
97
  @back="updateEditing(null)" @saved="updateMedia"
96
98
  @modified="editorModified"
@@ -100,6 +100,7 @@
100
100
  <script>
101
101
  import AposEditorMixin from 'Modules/@apostrophecms/modal/mixins/AposEditorMixin';
102
102
  import AposAdvisoryLockMixin from 'Modules/@apostrophecms/ui/mixins/AposAdvisoryLockMixin';
103
+ import AposModifiedMixin from 'Modules/@apostrophecms/ui/mixins/AposModifiedMixin';
103
104
  import { detectDocChange } from 'Modules/@apostrophecms/schema/lib/detectChange';
104
105
  import { klona } from 'klona';
105
106
  import dayjs from 'dayjs';
@@ -110,7 +111,7 @@ import cuid from 'cuid';
110
111
  dayjs.extend(advancedFormat);
111
112
 
112
113
  export default {
113
- mixins: [ AposEditorMixin, AposAdvisoryLockMixin ],
114
+ mixins: [ AposEditorMixin, AposAdvisoryLockMixin, AposModifiedMixin ],
114
115
  props: {
115
116
  media: {
116
117
  type: Object,
@@ -124,6 +125,10 @@ export default {
124
125
  return [];
125
126
  }
126
127
  },
128
+ isModified: {
129
+ type: Boolean,
130
+ default: false
131
+ },
127
132
  moduleLabels: {
128
133
  type: Object,
129
134
  default() {
@@ -152,11 +157,20 @@ export default {
152
157
  moduleOptions() {
153
158
  return window.apos.modules[this.activeMedia.type] || {};
154
159
  },
160
+ canLocalize() {
161
+ return (Object.keys(apos.i18n.locales).length > 1) && this.moduleOptions.localized && this.activeMedia._id;
162
+ },
155
163
  moreMenu() {
156
164
  const menu = [ {
157
165
  label: 'apostrophe:discardChanges',
158
166
  action: 'cancel'
159
167
  } ];
168
+ if (this.canLocalize) {
169
+ menu.push({
170
+ label: 'apostrophe:localize',
171
+ action: 'localize'
172
+ });
173
+ }
160
174
  if (this.activeMedia._id && !this.restoreOnly) {
161
175
  menu.push({
162
176
  label: 'apostrophe:archiveImage',
@@ -330,7 +344,7 @@ export default {
330
344
  this.$emit('back');
331
345
  },
332
346
  lockNotAvailable() {
333
- this.isModified = false;
347
+ this.$emit('modified', false);
334
348
  this.cancel();
335
349
  },
336
350
  updateActiveAttachment(attachment) {
@@ -338,6 +352,27 @@ export default {
338
352
  },
339
353
  viewMedia () {
340
354
  window.open(this.activeMedia.attachment._urls.original, '_blank');
355
+ },
356
+ async localize(media) {
357
+ // If there are changes warn the user before discarding them before
358
+ // the localize operation
359
+ if (this.isModified) {
360
+ if (!await this.confirmAndCancel()) {
361
+ return;
362
+ }
363
+
364
+ await this.cancel();
365
+ this.updateActiveDoc(this.activeMedia);
366
+ }
367
+ apos.bus.$emit('admin-menu-click', {
368
+ itemName: '@apostrophecms/i18n:localize',
369
+ props: {
370
+ doc: this.activeMedia
371
+ }
372
+ });
373
+ },
374
+ async close() {
375
+ await this.cancel();
341
376
  }
342
377
  }
343
378
  };
@@ -806,6 +806,16 @@ module.exports = {
806
806
  return (req, res, next) => req.user ? next() : passportSession(req, res, next);
807
807
  })()
808
808
  },
809
+ removeUserForDraftSharing: {
810
+ before: '@apostrophecms/i18n',
811
+ middleware(req, res, next) {
812
+ // Remove user to hide the admin UI, in order to simulate a logged-out page view
813
+ if (self.isShareDraftRequest(req)) {
814
+ delete req.user;
815
+ }
816
+ return next();
817
+ }
818
+ },
809
819
  honorLoginInvalidBefore: {
810
820
  before: '@apostrophecms/i18n',
811
821
  middleware(req, res, next) {
@@ -0,0 +1,323 @@
1
+ <template>
2
+ <AposModal
3
+ :modal="modal"
4
+ class="apos-share-draft"
5
+ data-apos-test="share-draft-modal"
6
+ v-on="{ esc: close }"
7
+ @no-modal="$emit('safe-close')"
8
+ @inactive="modal.active = false"
9
+ @show-modal="modal.showModal = true"
10
+ >
11
+ <template #main>
12
+ <AposModalBody>
13
+ <template #bodyMain>
14
+ <div class="apos-share-draft__header">
15
+ <h2 class="apos-share-draft__heading">
16
+ {{ $t('apostrophe:shareDraftHeader') }}
17
+ </h2>
18
+ <Close
19
+ class="apos-share-draft__close"
20
+ :title="$t('apostrophe:close')"
21
+ :size="18"
22
+ @click.prevent="close"
23
+ />
24
+ </div>
25
+ <div class="apos-share-draft__content">
26
+ <div class="apos-share-draft__toggle-wrapper">
27
+ <AposToggle
28
+ v-model="disabled"
29
+ class="apos-share-draft__toggle"
30
+ @toggle="toggle"
31
+ />
32
+ <p class="apos-share-draft__toggle-label">
33
+ {{ $t('apostrophe:shareDraftEnable') }}
34
+ </p>
35
+ </div>
36
+ <p class="apos-share-draft__description">
37
+ {{ $t('apostrophe:shareDraftDescription') }}
38
+ </p>
39
+ <transition
40
+ name="collapse"
41
+ :duration="200"
42
+ >
43
+ <div
44
+ class="apos-share-draft__url-block"
45
+ v-show="!disabled"
46
+ >
47
+ <input
48
+ v-model="shareUrl"
49
+ type="text"
50
+ disabled
51
+ class="apos-share-draft__url"
52
+ >
53
+ <a
54
+ href=""
55
+ class="apos-share-draft__link-copy"
56
+ @click.prevent="copy"
57
+ >
58
+ <LinkVariant
59
+ class="apos-share-draft__link-icon"
60
+ :title="$t('apostrophe:shareDraftCopyLink')"
61
+ :size="16"
62
+ />
63
+ &nbsp;{{ $t('apostrophe:shareDraftCopyLink') }}
64
+ </a>
65
+ </div>
66
+ </transition>
67
+ </div>
68
+ </template>
69
+ </AposModalBody>
70
+ </template>
71
+ </AposModal>
72
+ </template>
73
+
74
+ <script>
75
+ import Close from 'vue-material-design-icons/Close.vue';
76
+ import LinkVariant from 'vue-material-design-icons/LinkVariant.vue';
77
+
78
+ export default {
79
+ components: {
80
+ Close,
81
+ LinkVariant
82
+ },
83
+ props: {
84
+ doc: {
85
+ type: Object,
86
+ required: true
87
+ }
88
+ },
89
+ emits: [ 'safe-close' ],
90
+ data() {
91
+ return {
92
+ modal: {
93
+ active: false,
94
+ type: 'overlay',
95
+ showModal: false,
96
+ disableHeader: true,
97
+ trapFocus: true
98
+ },
99
+ shareUrl: '',
100
+ disabled: true
101
+ };
102
+ },
103
+ async mounted() {
104
+ this.modal.active = true;
105
+ await this.checkUrlProp();
106
+ await this.getAposShareKey();
107
+ },
108
+ methods: {
109
+ async copy() {
110
+ await navigator.clipboard.writeText(this.shareUrl);
111
+ },
112
+ async toggle() {
113
+ this.disabled = !this.disabled;
114
+
115
+ await this.setShareUrl();
116
+ },
117
+ async setShareUrl() {
118
+ try {
119
+ const { aposShareKey } = await apos.http.post(
120
+ `${apos.modules[this.doc.type].action}/${this.doc._id}/share`, {
121
+ busy: true,
122
+ body: {
123
+ share: !this.disabled
124
+ },
125
+ draft: true
126
+ }
127
+ );
128
+
129
+ if (this.disabled) {
130
+ setTimeout(() => {
131
+ this.shareUrl = '';
132
+ }, 200);
133
+ return;
134
+ }
135
+
136
+ if (!aposShareKey) {
137
+ return this.showError();
138
+ }
139
+
140
+ this.shareUrl = this.generateShareUrl(aposShareKey);
141
+ } catch {
142
+ if (this.disabled) {
143
+ this.shareUrl = '';
144
+ return;
145
+ }
146
+ await this.showError();
147
+ }
148
+ },
149
+ close() {
150
+ this.modal.showModal = false;
151
+ },
152
+ async checkUrlProp() {
153
+ if (!this.doc._url) {
154
+ await this.showError();
155
+ }
156
+ },
157
+ async getAposShareKey() {
158
+ try {
159
+ const { aposShareKey } = await apos.http.get(
160
+ `${apos.modules[this.doc.type].action}/${this.doc._id}`, {}
161
+ );
162
+
163
+ if (aposShareKey) {
164
+ this.disabled = false;
165
+ this.shareUrl = this.generateShareUrl(aposShareKey);
166
+ }
167
+ } catch {
168
+ await this.showError();
169
+ }
170
+ },
171
+ async showError() {
172
+ await apos.notify('apostrophe:shareDraftError', {
173
+ type: 'danger',
174
+ icon: 'alert-circle-icon',
175
+ dismiss: true
176
+ });
177
+ },
178
+ generateShareUrl(aposShareKey) {
179
+ const regex = /^https?:\/\//;
180
+ const docUrl = regex.test(this.doc._url)
181
+ ? this.doc._url
182
+ : `${location.origin}${this.doc._url}`;
183
+
184
+ const url = new URL(docUrl);
185
+
186
+ const urlInfo = {
187
+ url: url.href
188
+ };
189
+
190
+ apos.bus.$emit('shared-draft-link', urlInfo);
191
+
192
+ return apos.http.addQueryToUrl(urlInfo.url, {
193
+ ...(url.search ? apos.http.parseQuery(url.search) : {}),
194
+ aposShareKey,
195
+ aposShareId: this.doc._id
196
+ });
197
+ }
198
+ }
199
+ };
200
+ </script>
201
+
202
+ <style lang="scss" scoped>
203
+ .apos-share-draft {
204
+ z-index: $z-index-modal;
205
+ position: fixed;
206
+ top: 0;
207
+ right: 0;
208
+ bottom: 0;
209
+ left: 0;
210
+ display: flex;
211
+ align-items: center;
212
+ justify-content: center;
213
+ }
214
+
215
+ ::v-deep .apos-modal__inner {
216
+ top: auto;
217
+ right: auto;
218
+ bottom: auto;
219
+ left: auto;
220
+ max-width: 700px;
221
+ height: auto;
222
+ border-radius: 15px;
223
+ }
224
+
225
+ ::v-deep .apos-modal__overlay {
226
+ .apos-modal + .apos-share-draft & {
227
+ display: block;
228
+ }
229
+ }
230
+
231
+ ::v-deep .apos-modal__body {
232
+ padding: 20px;
233
+ }
234
+
235
+ ::v-deep .apos-modal__body-main {
236
+ display: flex;
237
+ flex-direction: column;
238
+ align-items: center;
239
+ }
240
+
241
+ .apos-share-draft__header {
242
+ display: flex;
243
+ justify-content: space-between;
244
+ width: 100%;
245
+ border-bottom: 1px solid var(--a-base-8);
246
+ padding: 0 20px 20px;
247
+ }
248
+
249
+ .apos-share-draft__heading {
250
+ @include type-title;
251
+ line-height: var(--a-line-tall);
252
+ margin: 0;
253
+ }
254
+
255
+ .apos-share-draft__close {
256
+ height: 18px;
257
+ align-self: center;
258
+ cursor: pointer;
259
+ }
260
+
261
+ .apos-share-draft__content {
262
+ margin-top: $spacing-double;
263
+ }
264
+
265
+ .apos-share-draft__toggle-wrapper {
266
+ display: flex;
267
+ align-items: center;
268
+ }
269
+
270
+ .apos-share-draft__toggle {
271
+ margin-right: 12px;
272
+ }
273
+
274
+ .apos-share-draft__toggle-label {
275
+ @include type-base;
276
+ max-width: 370px;
277
+ line-height: var(--a-line-tallest);
278
+ margin: 0;
279
+ }
280
+
281
+ .apos-share-draft__description {
282
+ @include type-small;
283
+ line-height: var(--a-line-tall);
284
+ max-width: 355px;
285
+ color: var(--a-base-2);
286
+ }
287
+
288
+ .apos-share-draft__url-block {
289
+ overflow: hidden;
290
+ height: 63px;
291
+ transition: height 200ms linear;
292
+
293
+ &.collapse-enter,
294
+ &.collapse-leave-to {
295
+ height: 0;
296
+ }
297
+ }
298
+
299
+ .apos-share-draft__url {
300
+ @include type-base;
301
+ width: 100%;
302
+ padding: 5px;
303
+ border-radius: 5px;
304
+ background-color: var(--a-base-9);
305
+ border: 1px solid var(--a-base-8);
306
+ box-sizing: border-box;
307
+ }
308
+
309
+ .apos-share-draft__link-icon {
310
+ height: 16px;
311
+ }
312
+
313
+ .apos-share-draft__link-copy {
314
+ @include type-base;
315
+ display: flex;
316
+ justify-content: flex-end;
317
+ align-items: center;
318
+ margin-top: $spacing-double;
319
+ text-decoration: none;
320
+ color: var(--a-primary);
321
+ font-weight: var(--a-weight-bold);;
322
+ }
323
+ </style>
@@ -0,0 +1,79 @@
1
+ import cuid from 'cuid';
2
+
3
+ export default {
4
+ data: () => ({
5
+ fieldErrors: {},
6
+ errorCount: 0
7
+
8
+ }),
9
+ mounted () {
10
+ this.prepErrors();
11
+ },
12
+ computed: {
13
+ errorTooltip() {
14
+ return this.errorCount
15
+ ? {
16
+ key: 'apostrophe:errorCount',
17
+ count: this.errorCount
18
+ } : null;
19
+ }
20
+ },
21
+ methods: {
22
+ updateFieldErrors(fieldState) {
23
+ this.tabKey = cuid();
24
+ for (const key in this.groups) {
25
+ this.groups[key].fields.forEach(field => {
26
+ if (fieldState[field]) {
27
+ this.fieldErrors[key][field] = fieldState[field].error;
28
+ }
29
+ });
30
+ }
31
+ this.updateErrorCount();
32
+ },
33
+ updateErrorCount() {
34
+ let count = 0;
35
+ for (const key in this.fieldErrors) {
36
+ for (const errKey in this.fieldErrors[key]) {
37
+ if (this.fieldErrors[key][errKey]) {
38
+ count++;
39
+ }
40
+ }
41
+ }
42
+ this.errorCount = count;
43
+ },
44
+ prepErrors() {
45
+ this.fieldErrors = Object.keys(this.groups).reduce((acc, name) => {
46
+ return {
47
+ ...acc,
48
+ [name]: {}
49
+ };
50
+ }, {});
51
+ },
52
+ focusNextError() {
53
+ let field;
54
+ for (const key in this.fieldErrors) {
55
+ for (const errKey in this.fieldErrors[key]) {
56
+ if (this.fieldErrors[key][errKey] && !field) {
57
+ field = this.schema.filter(item => {
58
+ return item.name === errKey;
59
+ })[0];
60
+
61
+ if (field.group.name !== 'utility') {
62
+ this.switchPane(field.group.name);
63
+ }
64
+
65
+ this.getAposSchema(field).scrollFieldIntoView(field.name);
66
+ }
67
+ }
68
+ }
69
+ },
70
+
71
+ getAposSchema(field) {
72
+ if (field.group.name === 'utility') {
73
+ return this.$refs.utilitySchema;
74
+ } else {
75
+ return this.$refs[field.group.name][0];
76
+ }
77
+ }
78
+ }
79
+ };
@@ -73,7 +73,8 @@ export default {
73
73
  getFieldsByCategory(followedByCategory) {
74
74
  if (followedByCategory) {
75
75
  return (followedByCategory === 'other')
76
- ? this.schema.filter(field => !this.utilityFields.includes(field.name)) : this.schema.filter(field => this.utilityFields.includes(field.name));
76
+ ? this.schema.filter(field => !this.utilityFields.includes(field.name))
77
+ : this.schema.filter(field => this.utilityFields.includes(field.name));
77
78
  } else {
78
79
  return this.schema;
79
80
  }
@@ -1,12 +1,70 @@
1
+ import cuid from 'cuid';
2
+
1
3
  // Provide basic bridging functionality between tabs
2
4
  // and the modal body.
3
5
 
4
6
  export default {
5
7
  data() {
6
8
  return {
9
+ tabKey: cuid(),
7
10
  currentTab: null
8
11
  };
9
12
  },
13
+ computed: {
14
+ groups() {
15
+ const groupSet = {};
16
+
17
+ this.schema.forEach(field => {
18
+ if (
19
+ this.filterOutParkedFields &&
20
+ !this.filterOutParkedFields([ field.name ]).length
21
+ ) {
22
+ return;
23
+ }
24
+ if (field.group && !groupSet[field.group.name]) {
25
+ groupSet[field.group.name] = {
26
+ label: field.group.label,
27
+ fields: [ field.name ],
28
+ schema: [ field ]
29
+ };
30
+ } else if (field.group) {
31
+ groupSet[field.group.name].fields.push(field.name);
32
+ groupSet[field.group.name].schema.push(field);
33
+ }
34
+ });
35
+ if (!groupSet.utility) {
36
+ groupSet.utility = {
37
+ label: 'apostrophe:utility',
38
+ fields: [],
39
+ schema: []
40
+ };
41
+ }
42
+
43
+ return groupSet;
44
+ },
45
+ tabs() {
46
+ const tabs = [];
47
+ for (const key in this.groups) {
48
+ if (key !== 'utility') {
49
+ tabs.push({
50
+ name: key,
51
+ label: this.groups[key].label
52
+ });
53
+ }
54
+ };
55
+
56
+ return tabs;
57
+ }
58
+ },
59
+
60
+ watch: {
61
+ tabs() {
62
+ if ((!this.currentTab) || (!this.tabs.find(tab => tab.name === this.currentTab))) {
63
+ this.currentTab = this.tabs[0] && this.tabs[0].name;
64
+ }
65
+ }
66
+ },
67
+
10
68
  mounted() {
11
69
  this.currentTab = this.tabs[0] ? this.tabs[0].name : null;
12
70
  },
@@ -746,6 +746,17 @@ module.exports = {
746
746
  }
747
747
  },
748
748
 
749
+ isShareDraftRequest(req) {
750
+ const { aposShareId, aposShareKey } = req.query;
751
+
752
+ return (
753
+ typeof aposShareId === 'string' &&
754
+ aposShareId.length &&
755
+ typeof aposShareKey === 'string' &&
756
+ aposShareKey.length
757
+ );
758
+ },
759
+
749
760
  // Merge in the event emitter / responder capabilities
750
761
  ...require('./lib/events.js')(self)
751
762
  };