apostrophe 3.43.0 → 3.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/force-deploy +1 -0
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +3 -2
- package/modules/@apostrophecms/doc-type/index.js +10 -0
- package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocContextMenu.vue +17 -3
- package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +18 -5
- package/modules/@apostrophecms/i18n/i18n/de.json +1 -0
- package/modules/@apostrophecms/i18n/i18n/en.json +1 -0
- package/modules/@apostrophecms/i18n/i18n/es.json +1 -0
- package/modules/@apostrophecms/i18n/i18n/fr.json +1 -0
- package/modules/@apostrophecms/i18n/i18n/pt-BR.json +1 -0
- package/modules/@apostrophecms/i18n/i18n/sk.json +1 -0
- package/modules/@apostrophecms/i18n/index.js +3 -0
- package/modules/@apostrophecms/i18n/ui/apos/components/AposI18nLocalize.vue +25 -13
- package/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerEditor.vue +1 -1
- package/modules/@apostrophecms/modal/ui/apos/mixins/AposEditorMixin.js +2 -1
- package/modules/@apostrophecms/modal/ui/apos/mixins/AposModalTabsMixin.js +3 -2
- package/modules/@apostrophecms/module/index.js +11 -0
- package/modules/@apostrophecms/page/index.js +13 -5
- package/modules/@apostrophecms/piece-type/index.js +3 -3
- package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManagerDisplay.vue +3 -3
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputCheckboxes.vue +27 -3
- package/modules/@apostrophecms/ui/ui/apos/components/AposCombo.vue +381 -0
- package/package.json +1 -1
- package/test/pieces-public-api.js +32 -0
- package/test/pieces.js +16 -48
- package/test-lib/util.js +52 -15
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.44.0 (2023-04-13)
|
|
4
|
+
|
|
5
|
+
### Adds
|
|
6
|
+
|
|
7
|
+
* `checkboxes` fields now support a new `style: 'combobox'` option for a better multiple-select experience when there
|
|
8
|
+
are many choices.
|
|
9
|
+
* If the new `guestApiAccess` option is set to `true` for a piece type or for `@apostrophecms/page`,
|
|
10
|
+
Apostrophe will allow all logged-in users to access the GET-method REST APIs of that
|
|
11
|
+
module, not just users with editing privileges, even if `publicApiProjection` is not set.
|
|
12
|
+
This is useful when the goal is to allow REST API access to "guest" users who have
|
|
13
|
+
project-specific reasons to fetch access content via REST APIs.
|
|
14
|
+
* `test-lib/utils.js` has new `createUser` and `loginAs` methods for the convenience of
|
|
15
|
+
those writing mocha tests of Apostrophe modules.
|
|
16
|
+
|
|
3
17
|
## 3.43.0 (2023-03-29)
|
|
4
18
|
|
|
5
19
|
### Adds
|
package/force-deploy
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
4
|
|
@@ -79,10 +79,11 @@ export default {
|
|
|
79
79
|
},
|
|
80
80
|
computed: {
|
|
81
81
|
contextBarActive() {
|
|
82
|
-
return window.apos.adminBar.contextBar && this.canEdit;
|
|
82
|
+
return window.apos.adminBar.contextBar && (this.canEdit || this.moduleOptions.canLocalize);
|
|
83
83
|
},
|
|
84
84
|
canEdit() {
|
|
85
|
-
return this.context._edit || ((this.context.aposLocale && this.context.aposLocale.endsWith(':published')) &&
|
|
85
|
+
return this.context._edit || ((this.context.aposLocale && this.context.aposLocale.endsWith(':published')) &&
|
|
86
|
+
this.draftIsEditable);
|
|
86
87
|
},
|
|
87
88
|
classes() {
|
|
88
89
|
if (!this.contextBarActive) {
|
|
@@ -763,6 +763,11 @@ module.exports = {
|
|
|
763
763
|
async findOneForCopying(req, criteria) {
|
|
764
764
|
return self.findOneForEditing(req, criteria);
|
|
765
765
|
},
|
|
766
|
+
// Identical to findOneForEditing by default, but could be
|
|
767
|
+
// overridden usefully in subclasses.
|
|
768
|
+
async findOneForLocalizing(req, criteria) {
|
|
769
|
+
return self.findOneForEditing(req, criteria);
|
|
770
|
+
},
|
|
766
771
|
// Submit the current draft for review. The identity
|
|
767
772
|
// of `req.user` is associated with the submission.
|
|
768
773
|
// Returns the `submitted` object, with `by`, `byId`,
|
|
@@ -1424,8 +1429,13 @@ module.exports = {
|
|
|
1424
1429
|
label,
|
|
1425
1430
|
pluralLabel,
|
|
1426
1431
|
relatedDocument: self.options.relatedDocument,
|
|
1432
|
+
canEdit: self.apos.permission.can(req, 'edit', self.name, 'draft'),
|
|
1427
1433
|
canPublish: self.apos.permission.can(req, 'publish', self.name)
|
|
1428
1434
|
};
|
|
1435
|
+
browserOptions.canLocalize = browserOptions.canEdit &&
|
|
1436
|
+
self.options.localized &&
|
|
1437
|
+
Object.keys(self.apos.i18n.locales).length > 1 &&
|
|
1438
|
+
Object.values(self.apos.i18n.locales).some(locale => locale._edit);
|
|
1429
1439
|
browserOptions.action = self.action;
|
|
1430
1440
|
browserOptions.schema = self.allowedSchema(req);
|
|
1431
1441
|
browserOptions.localized = self.isLocalized();
|
|
@@ -181,6 +181,10 @@ export default {
|
|
|
181
181
|
return menu;
|
|
182
182
|
},
|
|
183
183
|
customMenusByContext() {
|
|
184
|
+
if (!this.canEdit) {
|
|
185
|
+
return [];
|
|
186
|
+
}
|
|
187
|
+
|
|
184
188
|
const menus = this.customOperationsByContext
|
|
185
189
|
.map(op => ({
|
|
186
190
|
label: op.label,
|
|
@@ -233,7 +237,7 @@ export default {
|
|
|
233
237
|
}
|
|
234
238
|
},
|
|
235
239
|
canDismissSubmission() {
|
|
236
|
-
return this.context.submitted && (this.canPublish || (this.context.submitted.byId === apos.login.user._id));
|
|
240
|
+
return this.canEdit && this.context.submitted && (this.canPublish || (this.context.submitted.byId === apos.login.user._id));
|
|
237
241
|
},
|
|
238
242
|
canDiscardDraft() {
|
|
239
243
|
if (!this.manuallyPublished) {
|
|
@@ -242,6 +246,9 @@ export default {
|
|
|
242
246
|
if (!this.context._id) {
|
|
243
247
|
return false;
|
|
244
248
|
}
|
|
249
|
+
if (!this.canEdit) {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
245
252
|
return (
|
|
246
253
|
(!this.context.lastPublishedAt) &&
|
|
247
254
|
!this.moduleOptions.singleton
|
|
@@ -251,10 +258,12 @@ export default {
|
|
|
251
258
|
);
|
|
252
259
|
},
|
|
253
260
|
canLocalize() {
|
|
254
|
-
return
|
|
261
|
+
return this.moduleOptions.canLocalize &&
|
|
262
|
+
this.context._id;
|
|
255
263
|
},
|
|
256
264
|
canArchive() {
|
|
257
265
|
return (
|
|
266
|
+
this.canEdit &&
|
|
258
267
|
this.context._id &&
|
|
259
268
|
!this.moduleOptions.singleton &&
|
|
260
269
|
!this.context.archived &&
|
|
@@ -264,6 +273,7 @@ export default {
|
|
|
264
273
|
},
|
|
265
274
|
canUnpublish() {
|
|
266
275
|
return (
|
|
276
|
+
this.canEdit &&
|
|
267
277
|
!this.context.parked &&
|
|
268
278
|
this.moduleOptions.canPublish &&
|
|
269
279
|
this.context.lastPublishedAt &&
|
|
@@ -271,10 +281,14 @@ export default {
|
|
|
271
281
|
);
|
|
272
282
|
},
|
|
273
283
|
canCopy() {
|
|
274
|
-
return this.canEdit &&
|
|
284
|
+
return this.canEdit &&
|
|
285
|
+
this.moduleOptions.canEdit &&
|
|
286
|
+
!this.moduleOptions.singleton &&
|
|
287
|
+
this.context._id;
|
|
275
288
|
},
|
|
276
289
|
canRestore() {
|
|
277
290
|
return (
|
|
291
|
+
this.canEdit &&
|
|
278
292
|
this.context._id &&
|
|
279
293
|
this.context.archived &&
|
|
280
294
|
((this.moduleOptions.canPublish && this.context.lastPublishedAt) || !this.manuallyPublished)
|
|
@@ -162,6 +162,7 @@ export default {
|
|
|
162
162
|
ref: null
|
|
163
163
|
},
|
|
164
164
|
published: null,
|
|
165
|
+
readOnly: false,
|
|
165
166
|
restoreOnly: false,
|
|
166
167
|
saveMenu: null,
|
|
167
168
|
generation: 0
|
|
@@ -174,14 +175,24 @@ export default {
|
|
|
174
175
|
followingUtils() {
|
|
175
176
|
return this.followingValues('utility');
|
|
176
177
|
},
|
|
178
|
+
canEdit() {
|
|
179
|
+
if (this.original && this.original._id) {
|
|
180
|
+
return this.original._edit || this.moduleOptions.canEdit;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return this.moduleOptions.canEdit;
|
|
184
|
+
},
|
|
177
185
|
canPublish() {
|
|
178
186
|
if (this.original && this.original._id) {
|
|
179
|
-
return this.original._publish;
|
|
187
|
+
return this.original._publish || this.moduleOptions.canPublish;
|
|
180
188
|
}
|
|
181
189
|
|
|
182
190
|
return this.moduleOptions.canPublish;
|
|
183
191
|
},
|
|
184
192
|
saveDisabled() {
|
|
193
|
+
if (!this.canEdit) {
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
185
196
|
if (this.restoreOnly) {
|
|
186
197
|
// Can always restore if it's a read-only view of the archive
|
|
187
198
|
return false;
|
|
@@ -421,10 +432,6 @@ export default {
|
|
|
421
432
|
async loadDoc() {
|
|
422
433
|
let docData;
|
|
423
434
|
try {
|
|
424
|
-
if (!await this.lock(this.getOnePath, this.docId)) {
|
|
425
|
-
await this.lockNotAvailable();
|
|
426
|
-
return;
|
|
427
|
-
}
|
|
428
435
|
docData = await apos.http.get(this.getOnePath, {
|
|
429
436
|
busy: true,
|
|
430
437
|
qs: {
|
|
@@ -438,6 +445,12 @@ export default {
|
|
|
438
445
|
} else {
|
|
439
446
|
this.restoreOnly = false;
|
|
440
447
|
}
|
|
448
|
+
const canEdit = docData._edit || this.moduleOptions.canEdit;
|
|
449
|
+
this.readOnly = canEdit === false;
|
|
450
|
+
if (canEdit && !await this.lock(this.getOnePath, this.docId)) {
|
|
451
|
+
await this.lockNotAvailable();
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
441
454
|
} catch {
|
|
442
455
|
await apos.notify('apostrophe:loadDocFailed', {
|
|
443
456
|
type: 'warning',
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
"addWidgetType": "{{ label }} hinzufügen",
|
|
5
5
|
"admin": "Administrator",
|
|
6
6
|
"affirmativeLabel": "Ja, fortsetzen.",
|
|
7
|
+
"allSelected": "Alle ausgewählt",
|
|
7
8
|
"altText": "Alternativtext",
|
|
8
9
|
"altTextHelp": "Bildbeschreibung oder alternativer Text, der für Bildschirmleser verwendet wird.",
|
|
9
10
|
"any": "Irgendwas",
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
"addItem": "Ajouter un élément",
|
|
4
4
|
"addWidgetType": "Ajouter {{ label }}",
|
|
5
5
|
"affirmativeLabel": "Oui, continuer.",
|
|
6
|
+
"allSelected": "Tout Selectionné",
|
|
6
7
|
"altText": "Texte alternatif",
|
|
7
8
|
"altTextHelp": "Description d'image utilisée pour l'accessibilité",
|
|
8
9
|
"any": "Quelconque",
|
|
@@ -278,7 +278,8 @@ export default {
|
|
|
278
278
|
([ locale, options ]) => {
|
|
279
279
|
return {
|
|
280
280
|
name: locale,
|
|
281
|
-
label: options.label || locale
|
|
281
|
+
label: options.label || locale,
|
|
282
|
+
_edit: options._edit
|
|
282
283
|
};
|
|
283
284
|
}
|
|
284
285
|
),
|
|
@@ -382,12 +383,12 @@ export default {
|
|
|
382
383
|
: apos.modules[this.doc.type].action;
|
|
383
384
|
},
|
|
384
385
|
filteredLocales() {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
.includes(this.wizard.sections.selectLocales.filter.toLowerCase());
|
|
386
|
+
const matches = term =>
|
|
387
|
+
term
|
|
388
|
+
.toLowerCase()
|
|
389
|
+
.includes(this.wizard.sections.selectLocales.filter.toLowerCase());
|
|
390
390
|
|
|
391
|
+
return this.locales.filter(({ name, label }) => {
|
|
391
392
|
return matches(name) || matches(label);
|
|
392
393
|
});
|
|
393
394
|
},
|
|
@@ -395,7 +396,7 @@ export default {
|
|
|
395
396
|
return this.wizard.values.toLocales.data;
|
|
396
397
|
},
|
|
397
398
|
allSelected() {
|
|
398
|
-
return this.selectedLocales.length === this.locales.filter(locale => !this.isCurrentLocale(locale)).length;
|
|
399
|
+
return this.selectedLocales.length === this.locales.filter(locale => !this.isCurrentLocale(locale) && this.canEditLocale(locale)).length;
|
|
399
400
|
},
|
|
400
401
|
relatedDocTypes() {
|
|
401
402
|
const types = {};
|
|
@@ -532,6 +533,9 @@ export default {
|
|
|
532
533
|
isCurrentLocale(locale) {
|
|
533
534
|
return window.apos.i18n.locale === locale.name;
|
|
534
535
|
},
|
|
536
|
+
canEditLocale(locale) {
|
|
537
|
+
return !!locale._edit;
|
|
538
|
+
},
|
|
535
539
|
isSelected(locale) {
|
|
536
540
|
return this.wizard.values.toLocales.data.some(
|
|
537
541
|
({ name }) => name === locale.name
|
|
@@ -541,13 +545,13 @@ export default {
|
|
|
541
545
|
return !!this.localized[locale.name];
|
|
542
546
|
},
|
|
543
547
|
selectAll() {
|
|
544
|
-
this.wizard.values.toLocales.data = this.locales.filter(locale => !this.isCurrentLocale(locale));
|
|
548
|
+
this.wizard.values.toLocales.data = this.locales.filter(locale => !this.isCurrentLocale(locale) && this.canEditLocale(locale));
|
|
545
549
|
},
|
|
546
550
|
deselectAll() {
|
|
547
551
|
this.wizard.values.toLocales.data = [];
|
|
548
552
|
},
|
|
549
553
|
toggleLocale(locale) {
|
|
550
|
-
if (!this.isSelected(locale) && !this.isCurrentLocale(locale)) {
|
|
554
|
+
if (!this.isSelected(locale) && !this.isCurrentLocale(locale) && this.canEditLocale(locale)) {
|
|
551
555
|
this.wizard.values.toLocales.data.push(locale);
|
|
552
556
|
} else if (this.isSelected(locale)) {
|
|
553
557
|
this.wizard.values.toLocales.data = this.wizard.values.toLocales.data.filter(l => l !== locale);
|
|
@@ -590,6 +594,9 @@ export default {
|
|
|
590
594
|
if (this.isCurrentLocale(locale)) {
|
|
591
595
|
classes['apos-current-locale'] = true;
|
|
592
596
|
}
|
|
597
|
+
if (!this.canEditLocale(locale)) {
|
|
598
|
+
classes['apos-disabled-locale'] = true;
|
|
599
|
+
}
|
|
593
600
|
return classes;
|
|
594
601
|
},
|
|
595
602
|
// Singular type name for label (returns an i18next key)
|
|
@@ -948,15 +955,18 @@ export default {
|
|
|
948
955
|
line-height: 1;
|
|
949
956
|
border-radius: var(--a-border-radius);
|
|
950
957
|
|
|
951
|
-
&:not(.apos-current-locale)
|
|
958
|
+
&:not(.apos-current-locale),
|
|
959
|
+
&:not(.apos-disabled-locale) {
|
|
952
960
|
cursor: pointer;
|
|
953
961
|
}
|
|
954
962
|
|
|
955
|
-
&:not(.apos-current-locale):hover
|
|
963
|
+
&:not(.apos-current-locale):hover,
|
|
964
|
+
&:not(.apos-disabled-locale):hover {
|
|
956
965
|
background-color: var(--a-base-10);
|
|
957
966
|
}
|
|
958
967
|
|
|
959
|
-
&:not(.apos-current-locale):active
|
|
968
|
+
&:not(.apos-current-locale):active,
|
|
969
|
+
&:not(.apos-disabled-locale):active {
|
|
960
970
|
background-color: var(--a-base-9);
|
|
961
971
|
}
|
|
962
972
|
|
|
@@ -974,11 +984,13 @@ export default {
|
|
|
974
984
|
}
|
|
975
985
|
|
|
976
986
|
&.apos-current-locale,
|
|
987
|
+
&.apos-disabled-locale,
|
|
977
988
|
.apos-current-locale-icon {
|
|
978
989
|
color: var(--a-base-5);
|
|
979
990
|
}
|
|
980
991
|
|
|
981
|
-
&.apos-current-locale
|
|
992
|
+
&.apos-current-locale,
|
|
993
|
+
&.apos-disabled-locale {
|
|
982
994
|
font-style: italic;
|
|
983
995
|
}
|
|
984
996
|
|
|
@@ -158,7 +158,7 @@ export default {
|
|
|
158
158
|
return window.apos.modules[this.activeMedia.type] || {};
|
|
159
159
|
},
|
|
160
160
|
canLocalize() {
|
|
161
|
-
return
|
|
161
|
+
return this.moduleOptions.canLocalize && this.activeMedia._id;
|
|
162
162
|
},
|
|
163
163
|
moreMenu() {
|
|
164
164
|
const menu = [ {
|
|
@@ -24,6 +24,7 @@ export default {
|
|
|
24
24
|
},
|
|
25
25
|
serverErrors: null,
|
|
26
26
|
restoreOnly: false,
|
|
27
|
+
readOnly: false,
|
|
27
28
|
changed: [],
|
|
28
29
|
externalConditionsResults: {}
|
|
29
30
|
};
|
|
@@ -32,7 +33,7 @@ export default {
|
|
|
32
33
|
computed: {
|
|
33
34
|
schema() {
|
|
34
35
|
let schema = (this.moduleOptions.schema || []).filter(field => apos.schema.components.fields[field.type]);
|
|
35
|
-
if (this.restoreOnly) {
|
|
36
|
+
if (this.restoreOnly || this.readOnly) {
|
|
36
37
|
schema = klona(schema);
|
|
37
38
|
for (const field of schema) {
|
|
38
39
|
field.readOnly = true;
|
|
@@ -802,6 +802,17 @@ module.exports = {
|
|
|
802
802
|
}
|
|
803
803
|
},
|
|
804
804
|
|
|
805
|
+
// Modules that have REST APIs use this method
|
|
806
|
+
// to determine if a request is qualified to access
|
|
807
|
+
// it without restriction to the `publicApiProjection`
|
|
808
|
+
canAccessApi(req) {
|
|
809
|
+
if (self.options.guestApiAccess) {
|
|
810
|
+
return !!req.user;
|
|
811
|
+
} else {
|
|
812
|
+
return self.apos.permission.can(req, 'view-draft');
|
|
813
|
+
}
|
|
814
|
+
},
|
|
815
|
+
|
|
805
816
|
// Merge in the event emitter / responder capabilities
|
|
806
817
|
...require('./lib/events.js')(self)
|
|
807
818
|
};
|
|
@@ -133,7 +133,7 @@ module.exports = {
|
|
|
133
133
|
const autocomplete = self.apos.launder.string(req.query.autocomplete);
|
|
134
134
|
|
|
135
135
|
if (autocomplete.length) {
|
|
136
|
-
if (!self.apos.permission.can(req, '
|
|
136
|
+
if (!self.apos.permission.can(req, 'view', '@apostrophecms/any-page-type')) {
|
|
137
137
|
throw self.apos.error('forbidden');
|
|
138
138
|
}
|
|
139
139
|
return {
|
|
@@ -145,7 +145,7 @@ module.exports = {
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
if (all) {
|
|
148
|
-
if (!self.apos.permission.can(req, '
|
|
148
|
+
if (!self.apos.permission.can(req, 'view', '@apostrophecms/any-page-type')) {
|
|
149
149
|
throw self.apos.error('forbidden');
|
|
150
150
|
}
|
|
151
151
|
const page = await self.getRestQuery(req).permission(false).and({ level: 0 }).children({
|
|
@@ -418,7 +418,7 @@ module.exports = {
|
|
|
418
418
|
},
|
|
419
419
|
':_id/localize': async (req) => {
|
|
420
420
|
const _id = self.inferIdLocaleAndMode(req, req.params._id);
|
|
421
|
-
const draft = await self.
|
|
421
|
+
const draft = await self.findOneForLocalizing(req.clone({
|
|
422
422
|
mode: 'draft'
|
|
423
423
|
}), {
|
|
424
424
|
aposDocId: _id.split(':')[0]
|
|
@@ -819,6 +819,11 @@ database.`);
|
|
|
819
819
|
// A list of all valid page types, including parked pages etc. This is
|
|
820
820
|
// not a menu of choices for creating a page manually
|
|
821
821
|
browserOptions.validPageTypes = self.apos.instancesOf('@apostrophecms/page-type').map(module => module.__meta.name);
|
|
822
|
+
browserOptions.canEdit = self.apos.permission.can(req, 'edit', '@apostrophecms/any-page-type', 'draft');
|
|
823
|
+
browserOptions.canLocalize = browserOptions.canEdit &&
|
|
824
|
+
browserOptions.localized &&
|
|
825
|
+
Object.keys(self.apos.i18n.locales).length > 1 &&
|
|
826
|
+
Object.values(self.apos.i18n.locales).some(locale => locale._edit);
|
|
822
827
|
return browserOptions;
|
|
823
828
|
},
|
|
824
829
|
// Returns a query that finds pages the current user can edit
|
|
@@ -2212,7 +2217,7 @@ database.`);
|
|
|
2212
2217
|
.applyBuildersSafely(req.query);
|
|
2213
2218
|
// Minimum standard for a REST query without a public projection
|
|
2214
2219
|
// is being allowed to view drafts on the site
|
|
2215
|
-
if (!self.
|
|
2220
|
+
if (!self.canAccessApi(req)) {
|
|
2216
2221
|
if (!self.options.publicApiProjection) {
|
|
2217
2222
|
// Shouldn't be needed thanks to publicApiCheck, but be sure
|
|
2218
2223
|
query.and({
|
|
@@ -2243,6 +2248,9 @@ database.`);
|
|
|
2243
2248
|
async findOneForEditing(req, criteria, builders) {
|
|
2244
2249
|
return self.findForEditing(req, criteria, builders).toObject();
|
|
2245
2250
|
},
|
|
2251
|
+
async findOneForLocalizing(req, criteria, builders) {
|
|
2252
|
+
return self.findForEditing(req, criteria, builders).toObject();
|
|
2253
|
+
},
|
|
2246
2254
|
// Throws a `notfound` exception if a public API projection is
|
|
2247
2255
|
// not specified and the user does not have the `view-draft` permission,
|
|
2248
2256
|
// which all roles capable of editing the site at all will have. This is needed because
|
|
@@ -2250,7 +2258,7 @@ database.`);
|
|
|
2250
2258
|
// we also want to flunk all public access to REST APIs if not specifically configured.
|
|
2251
2259
|
publicApiCheck(req) {
|
|
2252
2260
|
if (!self.options.publicApiProjection) {
|
|
2253
|
-
if (!self.
|
|
2261
|
+
if (!self.canAccessApi(req)) {
|
|
2254
2262
|
throw self.apos.error('notfound');
|
|
2255
2263
|
}
|
|
2256
2264
|
}
|
|
@@ -408,7 +408,7 @@ module.exports = {
|
|
|
408
408
|
},
|
|
409
409
|
':_id/localize': async (req) => {
|
|
410
410
|
const _id = self.inferIdLocaleAndMode(req, req.params._id);
|
|
411
|
-
const draft = await self.
|
|
411
|
+
const draft = await self.findOneForLocalizing(req.clone({
|
|
412
412
|
mode: 'draft'
|
|
413
413
|
}), {
|
|
414
414
|
aposDocId: _id.split(':')[0]
|
|
@@ -1002,7 +1002,7 @@ module.exports = {
|
|
|
1002
1002
|
getRestQuery(req) {
|
|
1003
1003
|
const query = self.find(req).attachments(true);
|
|
1004
1004
|
query.applyBuildersSafely(req.query);
|
|
1005
|
-
if (!self.
|
|
1005
|
+
if (!self.canAccessApi(req)) {
|
|
1006
1006
|
if (!self.options.publicApiProjection) {
|
|
1007
1007
|
// Shouldn't be needed thanks to publicApiCheck, but be sure
|
|
1008
1008
|
query.and({
|
|
@@ -1024,7 +1024,7 @@ module.exports = {
|
|
|
1024
1024
|
// we also want to flunk all public access to REST APIs if not specifically configured.
|
|
1025
1025
|
publicApiCheck(req) {
|
|
1026
1026
|
if (!self.options.publicApiProjection) {
|
|
1027
|
-
if (!self.
|
|
1027
|
+
if (!self.canAccessApi(req)) {
|
|
1028
1028
|
throw self.apos.error('notfound');
|
|
1029
1029
|
}
|
|
1030
1030
|
}
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
/>
|
|
79
79
|
</td>
|
|
80
80
|
<!-- append the context menu -->
|
|
81
|
-
<td
|
|
81
|
+
<td class="apos-table__cell apos-table__cell--context-menu">
|
|
82
82
|
<AposCellContextMenu
|
|
83
83
|
:state="state[item._id]" :item="item"
|
|
84
84
|
:draft="item"
|
|
@@ -164,10 +164,10 @@ export default {
|
|
|
164
164
|
methods: {
|
|
165
165
|
canEdit(item) {
|
|
166
166
|
if (item._id) {
|
|
167
|
-
return item._edit;
|
|
167
|
+
return item._edit || this.options.canLocalize;
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
return this.options.canEdit;
|
|
170
|
+
return this.options.canEdit || this.options.canLocalize;
|
|
171
171
|
},
|
|
172
172
|
over(id) {
|
|
173
173
|
this.state[id].hover = true;
|
|
@@ -6,7 +6,16 @@
|
|
|
6
6
|
:display-options="displayOptions"
|
|
7
7
|
>
|
|
8
8
|
<template #body>
|
|
9
|
+
<AposCombo
|
|
10
|
+
v-if="field.style === 'combo' && choices.length"
|
|
11
|
+
:choices="choices"
|
|
12
|
+
:field="field"
|
|
13
|
+
:value="value"
|
|
14
|
+
@select-items="selectItems"
|
|
15
|
+
/>
|
|
16
|
+
|
|
9
17
|
<AposCheckbox
|
|
18
|
+
v-else
|
|
10
19
|
:for="getChoiceId(uid, choice.value)"
|
|
11
20
|
v-for="choice in choices"
|
|
12
21
|
:key="choice.value"
|
|
@@ -26,7 +35,7 @@ import AposInputChoicesMixin from 'Modules/@apostrophecms/schema/mixins/AposInpu
|
|
|
26
35
|
export default {
|
|
27
36
|
name: 'AposInputCheckboxes',
|
|
28
37
|
mixins: [ AposInputMixin, AposInputChoicesMixin ],
|
|
29
|
-
beforeMount
|
|
38
|
+
beforeMount () {
|
|
30
39
|
this.value.data = Array.isArray(this.value.data) ? this.value.data : [];
|
|
31
40
|
},
|
|
32
41
|
methods: {
|
|
@@ -49,12 +58,12 @@ export default {
|
|
|
49
58
|
|
|
50
59
|
if (this.field.min) {
|
|
51
60
|
if ((values != null) && (values.length < this.field.min)) {
|
|
52
|
-
return 'min
|
|
61
|
+
return this.$t('apostrophe:minUi', { number: this.field.min });
|
|
53
62
|
}
|
|
54
63
|
}
|
|
55
64
|
if (this.field.max) {
|
|
56
65
|
if ((values != null) && (values.length > this.field.max)) {
|
|
57
|
-
return 'max
|
|
66
|
+
return this.$t('apostrophe:maxUi', { number: this.field.max });
|
|
58
67
|
}
|
|
59
68
|
}
|
|
60
69
|
|
|
@@ -69,6 +78,21 @@ export default {
|
|
|
69
78
|
}
|
|
70
79
|
|
|
71
80
|
return false;
|
|
81
|
+
},
|
|
82
|
+
selectItems(choice) {
|
|
83
|
+
if (choice.value === '__all') {
|
|
84
|
+
this.value.data = this.choices.length === this.value.data.length
|
|
85
|
+
? []
|
|
86
|
+
: this.choices.map(({ value }) => value);
|
|
87
|
+
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (this.value.data.includes(choice.value)) {
|
|
92
|
+
this.value.data = this.value.data.filter((val) => val !== choice.value);
|
|
93
|
+
} else {
|
|
94
|
+
this.value.data.push(choice.value);
|
|
95
|
+
}
|
|
72
96
|
}
|
|
73
97
|
}
|
|
74
98
|
};
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="apos-primary-scrollbar apos-input-wrapper"
|
|
4
|
+
aria-haspopup="menu"
|
|
5
|
+
:class="{'apos-input-wrapper--disabled': field.readOnly}"
|
|
6
|
+
>
|
|
7
|
+
<ul
|
|
8
|
+
ref="select"
|
|
9
|
+
role="button"
|
|
10
|
+
:aria-expanded="showedList.toString()"
|
|
11
|
+
:aria-controls="`${field._id}-combo`"
|
|
12
|
+
v-click-outside-element="closeList"
|
|
13
|
+
class="apos-input-wrapper apos-combo__select"
|
|
14
|
+
@click="toggleList"
|
|
15
|
+
:tabindex="field.readOnly ? null : 0"
|
|
16
|
+
@keydown.prevent.space="toggleList"
|
|
17
|
+
@keydown.prevent.up="toggleList"
|
|
18
|
+
@keydown.prevent.down="toggleList"
|
|
19
|
+
>
|
|
20
|
+
<li
|
|
21
|
+
class="apos-combo__selected"
|
|
22
|
+
v-for="checked in selectedItems"
|
|
23
|
+
:key="checked"
|
|
24
|
+
@click="selectOption(getSelectedOption(checked))"
|
|
25
|
+
>
|
|
26
|
+
{{ getSelectedOption(checked)?.label }}
|
|
27
|
+
<AposIndicator
|
|
28
|
+
icon="close-icon"
|
|
29
|
+
class="apos-combo__close-icon"
|
|
30
|
+
:icon-size="10"
|
|
31
|
+
/>
|
|
32
|
+
</li>
|
|
33
|
+
</ul>
|
|
34
|
+
<AposIndicator
|
|
35
|
+
icon="menu-down-icon"
|
|
36
|
+
class="apos-input-icon"
|
|
37
|
+
:icon-size="20"
|
|
38
|
+
/>
|
|
39
|
+
<ul
|
|
40
|
+
:id="`${field._id}-combo`"
|
|
41
|
+
ref="list"
|
|
42
|
+
role="menu"
|
|
43
|
+
class="apos-combo__list"
|
|
44
|
+
:class="{'apos-combo__list--showed': showedList}"
|
|
45
|
+
:style="{top: boxHeight + 'px'}"
|
|
46
|
+
tabindex="0"
|
|
47
|
+
@keydown.prevent.space="selectOption(options[focusedItemIndex])"
|
|
48
|
+
@keydown.prevent.enter="selectOption(options[focusedItemIndex])"
|
|
49
|
+
@keydown.prevent.arrow-down="focusListItem()"
|
|
50
|
+
@keydown.prevent.arrow-up="focusListItem(true)"
|
|
51
|
+
@keydown.prevent.delete="closeList(null, true)"
|
|
52
|
+
@keydown.prevent.stop.esc="closeList(null, true)"
|
|
53
|
+
@blur="closeList()"
|
|
54
|
+
>
|
|
55
|
+
<li
|
|
56
|
+
:key="choice.value"
|
|
57
|
+
class="apos-combo__list-item"
|
|
58
|
+
role="menuitemcheckbox"
|
|
59
|
+
:class="{focused: focusedItemIndex === i}"
|
|
60
|
+
v-for="(choice, i) in options"
|
|
61
|
+
@click.stop="selectOption(choice)"
|
|
62
|
+
@mouseover="focusedItemIndex = i"
|
|
63
|
+
>
|
|
64
|
+
<AposIndicator
|
|
65
|
+
v-if="isSelected(choice)"
|
|
66
|
+
icon="check-bold-icon"
|
|
67
|
+
class="apos-combo__check-icon"
|
|
68
|
+
:icon-size="10"
|
|
69
|
+
/>
|
|
70
|
+
{{ choice.label }}
|
|
71
|
+
</li>
|
|
72
|
+
</ul>
|
|
73
|
+
</div>
|
|
74
|
+
</template>
|
|
75
|
+
|
|
76
|
+
<script>
|
|
77
|
+
export default {
|
|
78
|
+
name: 'AposCombo',
|
|
79
|
+
props: {
|
|
80
|
+
choices: {
|
|
81
|
+
type: Array,
|
|
82
|
+
required: true
|
|
83
|
+
},
|
|
84
|
+
field: {
|
|
85
|
+
type: Object,
|
|
86
|
+
required: true
|
|
87
|
+
},
|
|
88
|
+
value: {
|
|
89
|
+
type: Object,
|
|
90
|
+
required: true
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
emits: [ 'select-items' ],
|
|
95
|
+
data () {
|
|
96
|
+
const showSelectAll = this.field.all !== false &&
|
|
97
|
+
(!this.field.max || this.field.max > this.choices.length);
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
showedList: false,
|
|
101
|
+
boxHeight: 0,
|
|
102
|
+
showSelectAll,
|
|
103
|
+
options: this.renderOptions(showSelectAll),
|
|
104
|
+
boxResizeObserver: this.getBoxResizeObserver(),
|
|
105
|
+
focusedItemIndex: null
|
|
106
|
+
};
|
|
107
|
+
},
|
|
108
|
+
computed: {
|
|
109
|
+
selectedItems() {
|
|
110
|
+
if (this.allItemsSelected()) {
|
|
111
|
+
return [ '__all' ];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return this.value.data;
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
mounted() {
|
|
118
|
+
this.boxResizeObserver.observe(this.$refs.select);
|
|
119
|
+
},
|
|
120
|
+
beforeDestroy() {
|
|
121
|
+
this.boxResizeObserver.unobserve(this.$refs.select);
|
|
122
|
+
},
|
|
123
|
+
methods: {
|
|
124
|
+
toggleList() {
|
|
125
|
+
if (this.field.readOnly) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
this.showedList = !this.showedList;
|
|
129
|
+
|
|
130
|
+
if (this.showedList) {
|
|
131
|
+
this.$nextTick(() => {
|
|
132
|
+
this.$refs.list.focus();
|
|
133
|
+
this.focusedItemIndex = 0;
|
|
134
|
+
});
|
|
135
|
+
} else {
|
|
136
|
+
this.$refs.select.focus();
|
|
137
|
+
this.resetList();
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
closeList(_, focusSelect) {
|
|
141
|
+
this.showedList = false;
|
|
142
|
+
this.resetList();
|
|
143
|
+
|
|
144
|
+
if (focusSelect) {
|
|
145
|
+
this.$nextTick(() => {
|
|
146
|
+
this.$refs.select.focus();
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
resetList() {
|
|
151
|
+
this.focusedItemIndex = null;
|
|
152
|
+
this.$refs.list.scrollTo({ top: 0 });
|
|
153
|
+
},
|
|
154
|
+
getBoxResizeObserver() {
|
|
155
|
+
return new ResizeObserver(([ { target } ]) => {
|
|
156
|
+
if (target.offsetHeight !== this.boxHeight) {
|
|
157
|
+
this.boxHeight = target.offsetHeight;
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
},
|
|
161
|
+
renderOptions(showSelectAll) {
|
|
162
|
+
if (!showSelectAll) {
|
|
163
|
+
return this.choices;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const { listLabel } = this.getSelectAllLabel();
|
|
167
|
+
|
|
168
|
+
return [
|
|
169
|
+
{
|
|
170
|
+
label: listLabel,
|
|
171
|
+
value: '__all'
|
|
172
|
+
},
|
|
173
|
+
...this.choices
|
|
174
|
+
];
|
|
175
|
+
},
|
|
176
|
+
isSelected(choice) {
|
|
177
|
+
return this.value.data.some((val) => val === choice.value);
|
|
178
|
+
},
|
|
179
|
+
allItemsSelected () {
|
|
180
|
+
return this.choices.length && this.value.data.length === this.choices.length;
|
|
181
|
+
},
|
|
182
|
+
getSelectedOption(checked) {
|
|
183
|
+
if (checked === '__all') {
|
|
184
|
+
const { selectedLabel } = this.getSelectAllLabel();
|
|
185
|
+
return {
|
|
186
|
+
label: selectedLabel,
|
|
187
|
+
value: checked
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return this.choices.find((choice) => choice.value === checked);
|
|
192
|
+
},
|
|
193
|
+
emitSelectItems(data) {
|
|
194
|
+
return new Promise((resolve) => {
|
|
195
|
+
this.$emit('select-items', data);
|
|
196
|
+
this.$nextTick(resolve);
|
|
197
|
+
});
|
|
198
|
+
},
|
|
199
|
+
async selectOption(choice) {
|
|
200
|
+
if (this.field.readOnly) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const selectedChoice = this.showSelectAll && choice === '__all'
|
|
205
|
+
? this.getSelectedOption('__all')
|
|
206
|
+
: choice;
|
|
207
|
+
|
|
208
|
+
await this.emitSelectItems(selectedChoice);
|
|
209
|
+
|
|
210
|
+
if (this.showSelectAll) {
|
|
211
|
+
const { listLabel } = this.getSelectAllLabel();
|
|
212
|
+
this.options[0].label = listLabel;
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
getSelectAllLabel() {
|
|
217
|
+
const allSelected = this.allItemsSelected();
|
|
218
|
+
const defaultSelectAllListLabel = allSelected
|
|
219
|
+
? this.$t('apostrophe:deselectAll')
|
|
220
|
+
: this.$t('apostrophe:selectAll');
|
|
221
|
+
const selectedLabel = this.$t('apostrophe:allSelected');
|
|
222
|
+
|
|
223
|
+
if (this.field?.all?.label) {
|
|
224
|
+
const selectAllLabel = this.$t(this.field.all.label);
|
|
225
|
+
return {
|
|
226
|
+
selectedLabel,
|
|
227
|
+
listLabel: allSelected ? defaultSelectAllListLabel : selectAllLabel
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
selectedLabel,
|
|
233
|
+
listLabel: defaultSelectAllListLabel
|
|
234
|
+
};
|
|
235
|
+
},
|
|
236
|
+
focusListItem(prev = false) {
|
|
237
|
+
const destIndex = (i) => prev ? i - 1 : i + 1;
|
|
238
|
+
const fallback = prev ? this.options.length - 1 : 0;
|
|
239
|
+
if (this.focusedItemIndex == null) {
|
|
240
|
+
this.focusedItemIndex = fallback;
|
|
241
|
+
} else {
|
|
242
|
+
this.focusedItemIndex = this.options[destIndex(this.focusedItemIndex)]
|
|
243
|
+
? destIndex(this.focusedItemIndex)
|
|
244
|
+
: fallback;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const itemHeight = this.$refs.list.querySelector('li')?.clientHeight || 32;
|
|
248
|
+
const focusedItemPos = itemHeight * this.focusedItemIndex;
|
|
249
|
+
const { clientHeight, scrollTop } = this.$refs.list;
|
|
250
|
+
const listVisibility = clientHeight + scrollTop;
|
|
251
|
+
const scrollTo = (top) => {
|
|
252
|
+
this.$refs.list.scrollTo({
|
|
253
|
+
top,
|
|
254
|
+
behavior: 'smooth'
|
|
255
|
+
});
|
|
256
|
+
};
|
|
257
|
+
if (focusedItemPos + itemHeight > listVisibility) {
|
|
258
|
+
scrollTo((focusedItemPos + itemHeight) - clientHeight);
|
|
259
|
+
} else if (focusedItemPos < (listVisibility - clientHeight)) {
|
|
260
|
+
scrollTo(focusedItemPos);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
</script>
|
|
266
|
+
|
|
267
|
+
<style lang="scss" scoped>
|
|
268
|
+
.apos-combo__check-icon {
|
|
269
|
+
position: absolute;
|
|
270
|
+
left: 5px;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.apos-input-wrapper:focus {
|
|
274
|
+
.apos-combo__list {
|
|
275
|
+
display: block;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.apos-combo__select {
|
|
280
|
+
display: flex;
|
|
281
|
+
flex-wrap: wrap;
|
|
282
|
+
background-color: var(--a-base-9);
|
|
283
|
+
padding: 10px 30px 10px 8px;
|
|
284
|
+
min-height: 26px;
|
|
285
|
+
border: 1px solid var(--a-base-8);
|
|
286
|
+
border-radius: var(--a-border-radius);
|
|
287
|
+
cursor: pointer;
|
|
288
|
+
list-style: none;
|
|
289
|
+
|
|
290
|
+
&:hover {
|
|
291
|
+
border-color: var(--a-base-2);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
&:focus {
|
|
295
|
+
box-shadow: 0 0 3px var(--a-base-2);
|
|
296
|
+
border-color: var(--a-base-2);
|
|
297
|
+
background-color: var(--a-base-10);
|
|
298
|
+
outline: none;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.apos-input-wrapper--disabled {
|
|
303
|
+
.apos-combo__select {
|
|
304
|
+
color: var(--a-base-4);
|
|
305
|
+
background: var(--a-base-7);
|
|
306
|
+
border-color: var(--a-base-4);
|
|
307
|
+
cursor: not-allowed;
|
|
308
|
+
|
|
309
|
+
&:hover {
|
|
310
|
+
border-color: var(--a-base-4);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.apos-combo__selected {
|
|
315
|
+
background-color: var(--a-base-8);
|
|
316
|
+
border-color: var(--a-base-3);
|
|
317
|
+
opacity: 0.7;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.apos-input-icon {
|
|
321
|
+
opacity: 0.7;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.apos-combo__selected {
|
|
326
|
+
@include type-base;
|
|
327
|
+
display: flex;
|
|
328
|
+
align-items: center;
|
|
329
|
+
gap: 4px;
|
|
330
|
+
background-color: var(--a-white);
|
|
331
|
+
margin: 2px;
|
|
332
|
+
padding: 5px 8px;
|
|
333
|
+
border: 1px solid var(--a-base-8);
|
|
334
|
+
border-radius: var(--a-border-radius);
|
|
335
|
+
|
|
336
|
+
&:hover {
|
|
337
|
+
background-color: var(--a-base-8);
|
|
338
|
+
border-color: var(--a-base-3);
|
|
339
|
+
cursor: not-allowed;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
::v-deep .apos-indicator {
|
|
343
|
+
width: 10px;
|
|
344
|
+
height: 10px;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.apos-combo__list {
|
|
349
|
+
z-index: $z-index-manager-display;
|
|
350
|
+
position: absolute;
|
|
351
|
+
top: 44px;
|
|
352
|
+
left: 0;
|
|
353
|
+
display: none;
|
|
354
|
+
width: 100%;
|
|
355
|
+
list-style: none;
|
|
356
|
+
background-color: var(--a-white);
|
|
357
|
+
padding-left: 0;
|
|
358
|
+
margin: 0;
|
|
359
|
+
max-height: 300px;
|
|
360
|
+
overflow-y: auto;
|
|
361
|
+
box-shadow: 0 0 3px var(--a-base-2);
|
|
362
|
+
border-radius: var(--a-border-radius);
|
|
363
|
+
outline: none;
|
|
364
|
+
|
|
365
|
+
&--showed {
|
|
366
|
+
display: block;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.apos-combo__list-item {
|
|
371
|
+
@include type-base;
|
|
372
|
+
|
|
373
|
+
padding: 10px 10px 10px 20px;
|
|
374
|
+
cursor: pointer;
|
|
375
|
+
|
|
376
|
+
&.focused {
|
|
377
|
+
background-color: var(--a-base-9);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
</style>
|
package/package.json
CHANGED
|
@@ -72,6 +72,38 @@ describe('Pieces Public API', function() {
|
|
|
72
72
|
}
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
+
it('should not be able to retrieve a piece by id from the database without a public API projection as a guest', async function() {
|
|
76
|
+
await t.createUser(apos, 'guest');
|
|
77
|
+
const jar = await t.loginAs(apos, 'guest');
|
|
78
|
+
try {
|
|
79
|
+
await apos.http.get('/api/v1/thing', {
|
|
80
|
+
jar
|
|
81
|
+
});
|
|
82
|
+
// Bad, we expected a 404
|
|
83
|
+
assert(false);
|
|
84
|
+
} catch (e) {
|
|
85
|
+
assert(e.status === 404);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should be able to retrieve a piece by id from the database without a public API projection as a guest if guest API access is enabled', async function() {
|
|
90
|
+
let response;
|
|
91
|
+
try {
|
|
92
|
+
apos.modules.thing.options.guestApiAccess = true;
|
|
93
|
+
const jar = await t.loginAs(apos, 'guest');
|
|
94
|
+
response = await apos.http.get('/api/v1/thing', {
|
|
95
|
+
jar
|
|
96
|
+
});
|
|
97
|
+
} finally {
|
|
98
|
+
apos.modules.thing.options.guestApiAccess = false;
|
|
99
|
+
}
|
|
100
|
+
assert(response);
|
|
101
|
+
assert(response.results);
|
|
102
|
+
assert(response.results.length === 1);
|
|
103
|
+
assert(response.results[0].title === 'hello');
|
|
104
|
+
assert(response.results[0].foo === 'bar');
|
|
105
|
+
});
|
|
106
|
+
|
|
75
107
|
it('should be able to anonymously retrieve a piece by id from the database with a public API projection', async function() {
|
|
76
108
|
// Patch the option setting to simplify the test code
|
|
77
109
|
apos.thing.options.publicApiProjection = {
|
package/test/pieces.js
CHANGED
|
@@ -14,6 +14,10 @@ describe('Pieces', function() {
|
|
|
14
14
|
|
|
15
15
|
this.timeout(t.timeout);
|
|
16
16
|
|
|
17
|
+
let editor;
|
|
18
|
+
let contributor;
|
|
19
|
+
let guest;
|
|
20
|
+
|
|
17
21
|
before(async function() {
|
|
18
22
|
apos = await t.create({
|
|
19
23
|
root: module,
|
|
@@ -1996,36 +2000,6 @@ describe('Pieces', function() {
|
|
|
1996
2000
|
});
|
|
1997
2001
|
|
|
1998
2002
|
describe('field viewPermission|editPermission', function() {
|
|
1999
|
-
const createUser = (role = 'admin') => ({
|
|
2000
|
-
title = `test-${role}`,
|
|
2001
|
-
username = `test-${role}`,
|
|
2002
|
-
password = role
|
|
2003
|
-
} = {}) => {
|
|
2004
|
-
return apos.user.insert(
|
|
2005
|
-
apos.task.getReq(),
|
|
2006
|
-
{
|
|
2007
|
-
...apos.user.newInstance(),
|
|
2008
|
-
title,
|
|
2009
|
-
username,
|
|
2010
|
-
password,
|
|
2011
|
-
email: `${username}@admin.io`,
|
|
2012
|
-
role
|
|
2013
|
-
}
|
|
2014
|
-
);
|
|
2015
|
-
};
|
|
2016
|
-
const loginAs = async (role) => {
|
|
2017
|
-
const jar = apos.http.jar();
|
|
2018
|
-
await apos.http.post('/api/v1/@apostrophecms/login/login', {
|
|
2019
|
-
body: {
|
|
2020
|
-
username: `test-${role}`,
|
|
2021
|
-
password: role,
|
|
2022
|
-
session: true
|
|
2023
|
-
},
|
|
2024
|
-
jar
|
|
2025
|
-
});
|
|
2026
|
-
|
|
2027
|
-
return jar;
|
|
2028
|
-
};
|
|
2029
2003
|
|
|
2030
2004
|
this.afterEach(async function() {
|
|
2031
2005
|
await apos.doc.db.deleteMany({ email: /@admin.io$/ });
|
|
@@ -2058,8 +2032,8 @@ describe('Pieces', function() {
|
|
|
2058
2032
|
});
|
|
2059
2033
|
|
|
2060
2034
|
it('should be able to retrieve fields with viewPermission when having appropriate credentials on rest API', async function() {
|
|
2061
|
-
|
|
2062
|
-
const jar = await loginAs('admin');
|
|
2035
|
+
// admin user was inserted earlier
|
|
2036
|
+
const jar = await t.loginAs(apos, 'admin');
|
|
2063
2037
|
|
|
2064
2038
|
const req = apos.task.getReq();
|
|
2065
2039
|
const candidate = {
|
|
@@ -2089,10 +2063,8 @@ describe('Pieces', function() {
|
|
|
2089
2063
|
});
|
|
2090
2064
|
|
|
2091
2065
|
it('should be able to edit fields with editPermission when having appropriate credentials on rest API', async function() {
|
|
2092
|
-
await
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
const editor = await createUser('editor')();
|
|
2066
|
+
const jar = await t.loginAs(apos, 'admin');
|
|
2067
|
+
editor = await t.createUser(apos, 'editor');
|
|
2096
2068
|
|
|
2097
2069
|
const req = apos.task.getReq();
|
|
2098
2070
|
const candidate = {
|
|
@@ -2207,8 +2179,7 @@ describe('Pieces', function() {
|
|
|
2207
2179
|
});
|
|
2208
2180
|
|
|
2209
2181
|
it('should be able to retrieve fields with viewPermission when having appropriate credentials', async function() {
|
|
2210
|
-
await
|
|
2211
|
-
const jar = await loginAs('editor');
|
|
2182
|
+
const jar = await t.loginAs(apos, 'editor');
|
|
2212
2183
|
|
|
2213
2184
|
const req = apos.task.getReq();
|
|
2214
2185
|
const candidate = {
|
|
@@ -2238,8 +2209,7 @@ describe('Pieces', function() {
|
|
|
2238
2209
|
});
|
|
2239
2210
|
|
|
2240
2211
|
it('should be able to edit fields with editPermission when having appropriate credentials on rest API', async function() {
|
|
2241
|
-
await
|
|
2242
|
-
const jar = await loginAs('editor');
|
|
2212
|
+
const jar = await t.loginAs(apos, 'editor');
|
|
2243
2213
|
|
|
2244
2214
|
const req = apos.task.getReq();
|
|
2245
2215
|
const candidate = {
|
|
@@ -2339,8 +2309,8 @@ describe('Pieces', function() {
|
|
|
2339
2309
|
});
|
|
2340
2310
|
|
|
2341
2311
|
it('should be able to retrieve fields with viewPermission when having appropriate credentials', async function() {
|
|
2342
|
-
await createUser('contributor')
|
|
2343
|
-
const jar = await loginAs('contributor');
|
|
2312
|
+
contributor = await t.createUser(apos, 'contributor');
|
|
2313
|
+
const jar = await t.loginAs(apos, 'contributor');
|
|
2344
2314
|
|
|
2345
2315
|
const req = apos.task.getReq();
|
|
2346
2316
|
const candidate = {
|
|
@@ -2370,8 +2340,7 @@ describe('Pieces', function() {
|
|
|
2370
2340
|
});
|
|
2371
2341
|
|
|
2372
2342
|
it('should be able to edit fields with editPermission when having appropriate credentials on rest API', async function() {
|
|
2373
|
-
await
|
|
2374
|
-
const jar = await loginAs('contributor');
|
|
2343
|
+
const jar = await t.loginAs(apos, 'contributor');
|
|
2375
2344
|
|
|
2376
2345
|
const req = apos.task.getReq();
|
|
2377
2346
|
const candidate = {
|
|
@@ -2453,13 +2422,12 @@ describe('Pieces', function() {
|
|
|
2453
2422
|
});
|
|
2454
2423
|
|
|
2455
2424
|
it('should be able to edit with permission checks during prepareForStorage & updateCacheField handlers', async function() {
|
|
2456
|
-
const
|
|
2457
|
-
const jar = await loginAs('contributor');
|
|
2425
|
+
const jar = await t.loginAs(apos, 'contributor');
|
|
2458
2426
|
|
|
2459
|
-
|
|
2460
|
-
const guest = await createUser('guest')();
|
|
2427
|
+
guest = await t.createUser(apos, 'guest');
|
|
2461
2428
|
|
|
2462
2429
|
const req = apos.task.getReq({ mode: 'draft' });
|
|
2430
|
+
|
|
2463
2431
|
const candidate = {
|
|
2464
2432
|
...apos.modules.board.newInstance(),
|
|
2465
2433
|
title: 'Icarus',
|
package/test-lib/util.js
CHANGED
|
@@ -58,40 +58,77 @@ async function create(options) {
|
|
|
58
58
|
return require('../index.js')(config);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
// Create an admin user.
|
|
62
|
-
|
|
63
|
-
const user = apos.user.newInstance();
|
|
64
|
-
const name = username || 'admin';
|
|
61
|
+
// Create an admin user. Invokes createUser with the `admin` role.
|
|
62
|
+
// Accepts the same options.
|
|
65
63
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
64
|
+
async function createAdmin(apos, {
|
|
65
|
+
username, password, title, email
|
|
66
|
+
} = {}) {
|
|
67
|
+
return createUser(apos, 'admin', {
|
|
68
|
+
username,
|
|
69
|
+
password,
|
|
70
|
+
title,
|
|
71
|
+
email
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Resolves to a user with the specified `role`. `username` defaults to
|
|
76
|
+
// `role`. `title` defaults to `username` or `role`.
|
|
77
|
+
// `password` defaults to `username` or `role`. `email` defaults to `${username}@test.io`
|
|
78
|
+
// where `username` can also be inferred from `role`.
|
|
79
|
+
|
|
80
|
+
async function createUser(apos, role, {
|
|
81
|
+
username, password, title, email
|
|
82
|
+
} = {}) {
|
|
83
|
+
title = title || username || role;
|
|
84
|
+
username = username || role;
|
|
85
|
+
password = password || username || role;
|
|
86
|
+
email = email || `${username}@test.io`;
|
|
71
87
|
|
|
72
|
-
return
|
|
88
|
+
return apos.user.insert(
|
|
89
|
+
apos.task.getReq(),
|
|
90
|
+
{
|
|
91
|
+
...apos.user.newInstance(),
|
|
92
|
+
title,
|
|
93
|
+
username,
|
|
94
|
+
password,
|
|
95
|
+
email,
|
|
96
|
+
role
|
|
97
|
+
}
|
|
98
|
+
);
|
|
73
99
|
}
|
|
74
100
|
|
|
75
|
-
|
|
76
|
-
|
|
101
|
+
// Log in with the specified password. Returns a
|
|
102
|
+
// cookie jar object for use with additional
|
|
103
|
+
// apos.http calls via the `jar` option.
|
|
104
|
+
// `password` defaults to `username`.
|
|
77
105
|
|
|
78
|
-
|
|
106
|
+
async function loginAs(apos, username, password) {
|
|
107
|
+
password = password || username;
|
|
108
|
+
const jar = apos.http.jar();
|
|
79
109
|
await apos.http.post('/api/v1/@apostrophecms/login/login', {
|
|
80
110
|
body: {
|
|
81
|
-
username
|
|
82
|
-
password
|
|
111
|
+
username,
|
|
112
|
+
password,
|
|
83
113
|
session: true
|
|
84
114
|
},
|
|
85
115
|
jar
|
|
86
116
|
});
|
|
87
117
|
|
|
88
118
|
return jar;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Deprecated legacy wrapper for loginAs.
|
|
122
|
+
function getUserJar(apos, { username = 'admin', password } = {}) {
|
|
123
|
+
return loginAs(apos, username, password);
|
|
89
124
|
}
|
|
90
125
|
|
|
91
126
|
module.exports = {
|
|
92
127
|
destroy,
|
|
93
128
|
create,
|
|
94
129
|
createAdmin,
|
|
130
|
+
createUser,
|
|
131
|
+
loginAs,
|
|
95
132
|
getUserJar,
|
|
96
133
|
timeout: (process.env.TEST_TIMEOUT && parseInt(process.env.TEST_TIMEOUT)) || 20000
|
|
97
134
|
};
|