apostrophe 3.61.0 → 3.62.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 (36) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/modules/@apostrophecms/doc/index.js +2 -2
  3. package/modules/@apostrophecms/doc-type/index.js +8 -2
  4. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +7 -1
  5. package/modules/@apostrophecms/doc-type/ui/apos/logic/AposDocContextMenu.js +12 -3
  6. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManager.vue +8 -7
  7. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerDisplay.vue +1 -5
  8. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerEditor.vue +5 -2
  9. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerSelections.vue +1 -5
  10. package/modules/@apostrophecms/image/ui/apos/components/AposMediaUploader.vue +4 -2
  11. package/modules/@apostrophecms/page/index.js +33 -9
  12. package/modules/@apostrophecms/page/ui/apos/components/AposPagesManager.vue +3 -1
  13. package/modules/@apostrophecms/page/ui/apos/logic/AposPagesManager.js +7 -0
  14. package/modules/@apostrophecms/page-type/index.js +14 -2
  15. package/modules/@apostrophecms/permission/index.js +60 -31
  16. package/modules/@apostrophecms/piece-type/index.js +18 -7
  17. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +1 -1
  18. package/modules/@apostrophecms/rich-text-widget/index.js +141 -1
  19. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapLink.vue +38 -79
  20. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Link.js +11 -0
  21. package/modules/@apostrophecms/schema/lib/addFieldTypes.js +19 -2
  22. package/modules/@apostrophecms/schema/ui/apos/components/AposInputArea.vue +1 -1
  23. package/modules/@apostrophecms/schema/ui/apos/components/AposInputArray.vue +3 -0
  24. package/modules/@apostrophecms/schema/ui/apos/components/AposInputWrapper.vue +2 -1
  25. package/modules/@apostrophecms/schema/ui/apos/components/AposSchema.vue +1 -4
  26. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputRelationship.js +12 -8
  27. package/modules/@apostrophecms/ui/ui/apos/components/AposCellContextMenu.vue +1 -0
  28. package/modules/@apostrophecms/ui/ui/apos/components/AposCheckbox.vue +1 -0
  29. package/modules/@apostrophecms/ui/ui/apos/components/AposTree.vue +7 -0
  30. package/modules/@apostrophecms/ui/ui/apos/components/AposTreeRows.vue +8 -0
  31. package/modules/@apostrophecms/ui/ui/apos/scss/global/_inputs.scss +2 -2
  32. package/package.json +2 -2
  33. package/test/pages-autocomplete.js +203 -0
  34. package/test/permissions.js +76 -0
  35. package/test/rich-text-widget.js +164 -0
  36. package/test/schema-queryBuilders.js +180 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.62.0 (2024-01-25)
4
+
5
+ ### Adds
6
+
7
+ * Adds support for `type` query parameter for page autocomplete. This allows to filter the results by page type. Example: `/api/v1/@apostrophecms/page?autocomplete=something&type=my-page-type`.
8
+ * Add testing for the `float` schema field query builder.
9
+ * Add testing for the `integer` schema field query builder.
10
+ * Add support for link HTML attributes in the rich text widget via configurable fields `linkFields`, extendable on a project level (same as it's done for `fields`). Add an `htmlAttribute` property to the standard fields that map directly to an HTML attribute, except `href` (see special case below), and set it accordingly, even if it is the same as the field name. Setting `htmlAttribute: 'href'` is not allowed and will throw a schema validation exception (on application boot).
11
+ * Adds support in `can` and `criteria` methods for `create` and `delete`.
12
+ * Changes support for image upload from `canEdit` to `canCreate`.
13
+ * The media manager is compatible with per-doc permissions granted via the `@apostrophecms-pro/advanced-permission` module.
14
+
15
+ ### Fixes
16
+
17
+ * Fix the `launder` and `finalize` methods of the `float` schema field query builder.
18
+ * Fix the `launder` and `finalize` methods of the `integer` schema field query builder.
19
+ * A user who has permission to `publish` a particular page should always be allowed to insert it into the
20
+ published version of the site even if they could not otherwise insert a child of the published
21
+ parent.
22
+
23
+ ## 3.61.1 (2023-01-08)
24
+
25
+ ### Fixes
26
+
27
+ * Pinned Vue dependency to 2.7.15. Released on December 24th, Vue 2.7.16 broke the rich text toolbar in Apostrophe.
28
+
3
29
  ## 3.61.0 (2023-12-21)
4
30
 
5
31
  ### Adds
@@ -857,8 +857,8 @@ module.exports = {
857
857
  // Called by an `@apostrophecms/doc-type:insert` event handler to confirm that the user
858
858
  // has the appropriate permissions for the doc's type and content.
859
859
  testInsertPermissions(req, doc, options) {
860
- if (!(options.permissions === false)) {
861
- if (!self.apos.permission.can(req, 'edit', doc)) {
860
+ if (options.permissions !== false) {
861
+ if (!self.apos.permission.can(req, 'create', doc)) {
862
862
  throw self.apos.error('forbidden');
863
863
  }
864
864
  }
@@ -19,7 +19,9 @@ module.exports = {
19
19
  relationshipSuggestionIcon: 'text-box-icon',
20
20
  relationshipSuggestionFields: [ 'slug' ]
21
21
  },
22
- cascades: [ 'fields' ],
22
+ // Adding permissions for advanced permissions to allow modules to use it without
23
+ // being forced to check if the module is used with advanced permissions or not.
24
+ cascades: [ 'fields', 'permissions' ],
23
25
  fields(self) {
24
26
  return {
25
27
  add: {
@@ -1499,8 +1501,10 @@ module.exports = {
1499
1501
  label,
1500
1502
  pluralLabel,
1501
1503
  relatedDocument: self.options.relatedDocument,
1504
+ canCreate: self.apos.permission.can(req, 'create', self.name, 'draft'),
1502
1505
  canEdit: self.apos.permission.can(req, 'edit', self.name, 'draft'),
1503
- canPublish: self.apos.permission.can(req, 'publish', self.name)
1506
+ canPublish: self.apos.permission.can(req, 'publish', self.name),
1507
+ canArchive: self.apos.permission.can(req, 'delete', self.name)
1504
1508
  };
1505
1509
  browserOptions.canLocalize = browserOptions.canEdit &&
1506
1510
  self.options.localized &&
@@ -1868,8 +1872,10 @@ module.exports = {
1868
1872
  after(results) {
1869
1873
  // In all cases we mark the docs with ._edit and ._publish if
1870
1874
  // the req is permitted to do those things
1875
+ self.apos.permission.annotate(query.req, 'create', results);
1871
1876
  self.apos.permission.annotate(query.req, 'edit', results);
1872
1877
  self.apos.permission.annotate(query.req, 'publish', results);
1878
+ self.apos.permission.annotate(query.req, 'delete', results);
1873
1879
  }
1874
1880
  },
1875
1881
 
@@ -19,6 +19,7 @@
19
19
  :current="docFields.data"
20
20
  :published="published"
21
21
  :show-edit="false"
22
+ :can-delete-draft="moduleOptions.canDeleteDraft"
22
23
  @close="close"
23
24
  />
24
25
  <AposButton
@@ -195,8 +196,13 @@ export default {
195
196
 
196
197
  return this.moduleOptions.canPublish;
197
198
  },
199
+ canCreate() {
200
+ return this.original &&
201
+ !this.original._id &&
202
+ this.moduleOptions.canCreate;
203
+ },
198
204
  saveDisabled() {
199
- if (!this.canEdit) {
205
+ if (!this.canCreate && !this.canEdit) {
200
206
  return true;
201
207
  }
202
208
  if (this.restoreOnly) {
@@ -48,6 +48,12 @@ export default {
48
48
  return true;
49
49
  }
50
50
  },
51
+ canDeleteDraft: {
52
+ type: Boolean,
53
+ default() {
54
+ return true;
55
+ }
56
+ },
51
57
  showCopy: {
52
58
  type: Boolean,
53
59
  default() {
@@ -245,7 +251,10 @@ export default {
245
251
  if (!this.context._id) {
246
252
  return false;
247
253
  }
248
- if (!this.canEdit) {
254
+ if (!this.context.lastPublishedAt && !this.canDeleteDraft && !this.context._delete) {
255
+ return false;
256
+ }
257
+ if (this.context.lastPublishedAt && (!this.context.modified || !this.context._edit)) {
249
258
  return false;
250
259
  }
251
260
  return (
@@ -262,12 +271,12 @@ export default {
262
271
  },
263
272
  canArchive() {
264
273
  return (
265
- this.canEdit &&
274
+ this.context._delete &&
266
275
  this.context._id &&
267
276
  !this.moduleOptions.singleton &&
268
277
  !this.context.archived &&
269
278
  !this.context.parked &&
270
- ((this.moduleOptions.canPublish && this.context.lastPublishedAt) || !this.manuallyPublished)
279
+ (Boolean(this.canPublish && this.context.lastPublishedAt) || !this.manuallyPublished)
271
280
  );
272
281
  },
273
282
  canUnpublish() {
@@ -71,7 +71,6 @@
71
71
  :accept="accept"
72
72
  :items="items"
73
73
  :module-options="moduleOptions"
74
- :can-edit="moduleOptions.canEdit"
75
74
  @edit="updateEditing"
76
75
  v-model="checked"
77
76
  @select="select"
@@ -106,7 +105,6 @@
106
105
  />
107
106
  <AposMediaManagerSelections
108
107
  :items="selected"
109
- :can-edit="moduleOptions.canEdit"
110
108
  @clear="clearSelected" @edit="updateEditing"
111
109
  v-show="!editing"
112
110
  />
@@ -248,7 +246,8 @@ export default {
248
246
  async getMedia (options) {
249
247
  const qs = {
250
248
  ...this.filterValues,
251
- page: this.currentPage
249
+ page: this.currentPage,
250
+ viewContext: this.relationshipField ? 'relationship' : 'manage'
252
251
  };
253
252
  const filtered = !!Object.keys(this.filterValues).length;
254
253
  if (this.moduleOptions && Array.isArray(this.moduleOptions.filters)) {
@@ -354,9 +353,7 @@ export default {
354
353
  this.editing = undefined;
355
354
  },
356
355
  async updateEditing(id) {
357
- if (!this.moduleOptions.canEdit) {
358
- return;
359
- }
356
+ const item = this.items.find(item => item._id === id);
360
357
  // We only care about the current doc for this prompt,
361
358
  // we are not in danger of discarding a selection when
362
359
  // we switch images
@@ -371,7 +368,11 @@ export default {
371
368
  return false;
372
369
  }
373
370
  }
374
- this.editing = this.items.find(item => item._id === id);
371
+ if (!item?._edit) {
372
+ this.editing = null;
373
+ return true;
374
+ }
375
+ this.editing = item;
375
376
  return true;
376
377
  },
377
378
  // select setters
@@ -2,7 +2,7 @@
2
2
  <div class="apos-media-manager-display">
3
3
  <div class="apos-media-manager-display__grid">
4
4
  <AposMediaUploader
5
- v-if="canEdit"
5
+ v-if="moduleOptions.canCreate"
6
6
  :disabled="maxReached"
7
7
  :action="moduleOptions.action"
8
8
  :accept="accept"
@@ -83,10 +83,6 @@ export default {
83
83
  event: 'change'
84
84
  },
85
85
  props: {
86
- canEdit: {
87
- type: Boolean,
88
- default: false
89
- },
90
86
  maxReached: {
91
87
  type: Boolean,
92
88
  default: false
@@ -171,7 +171,7 @@ export default {
171
171
  action: 'localize'
172
172
  });
173
173
  }
174
- if (this.activeMedia._id && !this.restoreOnly) {
174
+ if (this.activeMedia._id && this.activeMedia._delete && !this.restoreOnly) {
175
175
  menu.push({
176
176
  label: 'apostrophe:archiveImage',
177
177
  action: 'archive',
@@ -209,7 +209,9 @@ export default {
209
209
  return dayjs(this.activeMedia.attachment.createdAt).format(this.$t('apostrophe:dayjsMediaCreatedDateFormat'));
210
210
  },
211
211
  isArchived() {
212
- return this.media.archived;
212
+ // ?. necessary to avoid reference to null due to
213
+ // race condition when toggling selection off
214
+ return this.media?.archived;
213
215
  }
214
216
  },
215
217
  watch: {
@@ -248,6 +250,7 @@ export default {
248
250
  this[action]();
249
251
  },
250
252
  async updateActiveDoc(newMedia) {
253
+ newMedia = newMedia || {};
251
254
  this.showReplace = false;
252
255
  this.activeMedia = klona(newMedia);
253
256
  this.restoreOnly = !!this.activeMedia.archived;
@@ -30,7 +30,7 @@
30
30
  {{ item.title }}
31
31
  </div>
32
32
  <AposButton
33
- v-if="canEdit"
33
+ v-if="item._edit"
34
34
  label="apostrophe:edit"
35
35
  type="quiet"
36
36
  :modifiers="['no-motion']"
@@ -49,10 +49,6 @@
49
49
  <script>
50
50
  export default {
51
51
  props: {
52
- canEdit: {
53
- type: Boolean,
54
- default: false
55
- },
56
52
  items: {
57
53
  type: Array,
58
54
  required: true
@@ -141,9 +141,11 @@ export default {
141
141
  for (const file of files) {
142
142
  try {
143
143
  const img = await this.insertImage(file, emptyDoc);
144
- imageIds.push(img._id);
144
+ if (img?._id) {
145
+ imageIds.push(img._id);
146
+ }
145
147
  } catch (e) {
146
- const msg = e.body && e.body.message ? e.body.message : this.$t('Upload error');
148
+ const msg = e.body && e.body.message ? e.body.message : this.$t('apostrophe:uploadError');
147
149
  await apos.notify(msg, {
148
150
  type: 'danger',
149
151
  icon: 'alert-circle-icon',
@@ -122,6 +122,9 @@ module.exports = {
122
122
  // The user must have some page editing privileges to use it. The 10 best
123
123
  // matches are returned as an object with a `results` property containing the
124
124
  // array of pages.
125
+ // If ?type=x is present, only pages of that type are returned. This query
126
+ // parameter is only used in conjunction with ?autocomplete=x. It will be
127
+ // ignored otherwise.
125
128
  //
126
129
  // If querying for draft pages, you may add ?published=1 to attach a
127
130
  // `_publishedDoc` property to each draft that also exists in a published form.
@@ -139,11 +142,22 @@ module.exports = {
139
142
  if (!self.apos.permission.can(req, 'view', '@apostrophecms/any-page-type')) {
140
143
  throw self.apos.error('forbidden');
141
144
  }
145
+
146
+ const type = self.apos.launder.string(req.query.type);
147
+ if (type.length && !self.apos.permission.can(req, 'view', type)) {
148
+ throw self.apos.error('forbidden');
149
+ }
150
+
151
+ const query = self.getRestQuery(req).permission(false).limit(10).relationships(false)
152
+ .areas(false);
153
+ if (type.length) {
154
+ query.type(type);
155
+ }
156
+
142
157
  return {
143
158
  // For consistency with the pieces REST API we
144
159
  // use a results property when returning a flat list
145
- results: await self.getRestQuery(req).permission(false).limit(10).relationships(false)
146
- .areas(false).toArray()
160
+ results: await query.toArray()
147
161
  };
148
162
  }
149
163
 
@@ -275,7 +289,7 @@ module.exports = {
275
289
  // If we're looking for a fresh page instance and aren't saving yet,
276
290
  // simply get a new page doc and return it
277
291
  const parentPage = await self.findForEditing(req, self.getIdCriteria(targetId))
278
- .permission('edit', '@apostrophecms/any-page-type').toObject();
292
+ .permission('create', '@apostrophecms/any-page-type').toObject();
279
293
  const newChild = self.newChild(parentPage);
280
294
  newChild._previewable = true;
281
295
  return newChild;
@@ -285,7 +299,7 @@ module.exports = {
285
299
  const targetPage = await self
286
300
  .findForEditing(req, self.getIdCriteria(targetId))
287
301
  .ancestors(true)
288
- .permission('edit')
302
+ .permission('create')
289
303
  .toObject();
290
304
 
291
305
  if (!targetPage) {
@@ -311,7 +325,10 @@ module.exports = {
311
325
  copyingId
312
326
  });
313
327
  await self.insert(req, targetPage._id, position, page, { lock: false });
314
- return self.findOneForEditing(req, { _id: page._id }, { attachments: true });
328
+ return self.findOneForEditing(req, { _id: page._id }, {
329
+ attachments: true,
330
+ permission: false
331
+ });
315
332
  });
316
333
  },
317
334
  // Consider using `PATCH` instead unless you're sure you have 100% up to date
@@ -391,6 +408,10 @@ module.exports = {
391
408
  const page = await self.findOneForEditing(req, {
392
409
  _id
393
410
  });
411
+
412
+ if (!page) {
413
+ throw self.apos.error('notfound');
414
+ }
394
415
  return self.delete(req, page);
395
416
  },
396
417
  // Patch some properties of the page.
@@ -828,7 +849,8 @@ database.`);
828
849
  }
829
850
  browserOptions.name = self.__meta.name;
830
851
  browserOptions.canPublish = self.apos.permission.can(req, 'publish', '@apostrophecms/any-page-type');
831
- browserOptions.quickCreate = self.options.quickCreate && self.apos.permission.can(req, 'edit', '@apostrophecms/any-page-type', 'draft');
852
+ browserOptions.canCreate = self.apos.permission.can(req, 'create', '@apostrophecms/any-page-type', 'draft');
853
+ browserOptions.quickCreate = self.options.quickCreate && self.apos.permission.can(req, 'create', '@apostrophecms/any-page-type', 'draft');
832
854
  browserOptions.localized = true;
833
855
  browserOptions.autopublish = false;
834
856
  // A list of all valid page types, including parked pages etc. This is
@@ -840,6 +862,8 @@ database.`);
840
862
  Object.keys(self.apos.i18n.locales).length > 1 &&
841
863
  Object.values(self.apos.i18n.locales).some(locale => locale._edit);
842
864
  browserOptions.utilityOperations = self.utilityOperations;
865
+ browserOptions.canDeleteDraft = self.apos.permission.can(req, 'delete', '@apostrophecms/any-page-type', 'draft');
866
+
843
867
  return browserOptions;
844
868
  },
845
869
  // Returns a query that finds pages the current user can edit
@@ -882,7 +906,7 @@ database.`);
882
906
  if ((position === 'before') || (position === 'after')) {
883
907
  parent = await self.findForEditing(req, {
884
908
  path: self.getParentPath(target)
885
- }).children({
909
+ }, { permission: 'create' }).children({
886
910
  depth: 1,
887
911
  archived: null,
888
912
  orphan: null,
@@ -898,7 +922,7 @@ database.`);
898
922
  throw self.apos.error('notfound');
899
923
  }
900
924
  if (options.permissions !== false) {
901
- if (!parent._edit) {
925
+ if (!parent._create) {
902
926
  throw self.apos.error('forbidden');
903
927
  }
904
928
  }
@@ -1095,7 +1119,7 @@ database.`);
1095
1119
  // Move outside tree
1096
1120
  throw self.apos.error('forbidden');
1097
1121
  }
1098
- if ((oldParent._id !== parent._id) && (parent.type !== '@apostrophecms/archive-page') && (!parent._edit)) {
1122
+ if ((oldParent._id !== parent._id) && (parent.type !== '@apostrophecms/archive-page') && (!parent._create)) {
1099
1123
  throw self.apos.error('forbidden');
1100
1124
  }
1101
1125
  if (moved.lastPublishedAt && !parent.lastPublishedAt) {
@@ -32,7 +32,7 @@
32
32
  :button="moreMenuButton"
33
33
  />
34
34
  <AposButton
35
- v-else type="primary"
35
+ v-else-if="canCreate" type="primary"
36
36
  label="apostrophe:newPage" @click="create()"
37
37
  />
38
38
  <AposButton
@@ -81,6 +81,7 @@
81
81
  :icons="icons"
82
82
  v-model="checked"
83
83
  :options="treeOptions"
84
+ :module-options="moduleOptions"
84
85
  @update="update"
85
86
  />
86
87
  </template>
@@ -98,6 +99,7 @@ export default {
98
99
  // Keep it for linting
99
100
  emits: [ 'archive', 'search', 'safe-close', 'modal-result' ]
100
101
  };
102
+ // TODO: check when child page is created and with what perm
101
103
  </script>
102
104
 
103
105
  <style lang="scss" scoped>
@@ -128,6 +128,13 @@ export default {
128
128
  },
129
129
  pageSetMenuSelectionIsLive() {
130
130
  return this.pageSetMenuSelection === 'live';
131
+ },
132
+ canCreate() {
133
+ const page = this.pagesFlat.find(page => page.aposDocId === this.moduleOptions.page.aposDocId);
134
+ if (page) {
135
+ return page._create;
136
+ }
137
+ return this.moduleOptions.canCreate;
131
138
  }
132
139
  },
133
140
  watch: {
@@ -104,7 +104,7 @@ module.exports = {
104
104
  },
105
105
  beforeMove: {
106
106
  checkPermissions(req, doc) {
107
- if (doc.lastPublishedAt && !self.apos.permission.can(req, 'publish', '@apostrophecms/any-page-type')) {
107
+ if (doc.lastPublishedAt && !self.apos.permission.can(req, 'publish', doc)) {
108
108
  throw self.apos.error('forbidden', 'Contributors may only move unpublished pages.');
109
109
  }
110
110
  }
@@ -352,6 +352,11 @@ module.exports = {
352
352
  // Called for you when a page is published for the first time.
353
353
  // You don't need to invoke this.
354
354
  async insertPublishedOf(req, doc, published, options = {}) {
355
+ // Check publish permission up front because we won't check it
356
+ // in insert
357
+ if (!self.apos.permission.can(req, 'publish', doc)) {
358
+ throw self.apos.error('forbidden');
359
+ }
355
360
  const _req = req.clone({
356
361
  mode: 'published'
357
362
  });
@@ -363,7 +368,14 @@ module.exports = {
363
368
  lastTargetId.replace(':draft', ':published'),
364
369
  lastPosition,
365
370
  published,
366
- options);
371
+ {
372
+ ...options,
373
+ // We already confirmed we are allowed to
374
+ // publish the draft, bypass checks that
375
+ // can get hung up on "create" permission
376
+ permissions: false
377
+ }
378
+ );
367
379
  } else {
368
380
  // Insert the home page
369
381
  Object.assign(published, {
@@ -59,6 +59,7 @@ module.exports = {
59
59
  if (role === 'admin') {
60
60
  return true;
61
61
  }
62
+
62
63
  const type = docOrType && (docOrType.type || docOrType);
63
64
  const doc = (docOrType && docOrType._id) ? docOrType : null;
64
65
  const manager = type && self.apos.doc.getManager(type);
@@ -66,46 +67,73 @@ module.exports = {
66
67
  self.apos.util.warn('A permission.can() call was made with a type that has no manager:', type);
67
68
  return false;
68
69
  }
70
+
69
71
  if (action === 'view') {
70
- if (manager && manager.options.viewRole && (ranks[role] < ranks[manager.options.viewRole])) {
71
- return false;
72
- } else if (((typeof docOrType) === 'object') && (docOrType.visibility !== 'public')) {
73
- return (role === 'guest') || (role === 'contributor') || (role === 'editor');
74
- } else {
75
- return true;
76
- }
77
- } else if (action === 'view-draft') {
72
+ return canView();
73
+ }
74
+
75
+ if (action === 'view-draft') {
78
76
  // Checked at the middleware level to determine if req.mode should
79
77
  // be allowed to be set to draft at all
80
78
  return (role === 'contributor') || (role === 'editor');
81
- } else if (action === 'edit') {
82
- if (manager && manager.options.editRole && (ranks[role] < ranks[manager.options.editRole])) {
79
+ }
80
+
81
+ if ([ 'edit', 'create' ].includes(action)) {
82
+ return canEdit();
83
+ }
84
+
85
+ if (action === 'publish') {
86
+ return canPublish();
87
+ }
88
+
89
+ if (action === 'upload-attachment') {
90
+ return (role === 'contributor') || (role === 'editor');
91
+ }
92
+
93
+ if (action === 'delete') {
94
+ return canDelete();
95
+ }
96
+
97
+ throw self.apos.error('invalid', 'That action is not implemented');
98
+
99
+ function checkRoleConfig (permRole) {
100
+ return manager && manager.options[permRole] &&
101
+ (ranks[role] < ranks[manager.options[permRole]]);
102
+ }
103
+
104
+ function canView() {
105
+ if (checkRoleConfig('viewRole')) {
83
106
  return false;
84
- } else if (mode === 'draft') {
85
- return (role === 'contributor') || (role === 'editor');
86
- } else {
87
- return role === 'editor';
88
107
  }
89
- } else if (action === 'publish') {
90
- if (manager && manager.options.publishRole && (ranks[role] < ranks[manager.options.publishRole])) {
108
+ if ((typeof docOrType === 'object') && (docOrType.visibility !== 'public')) {
109
+ return (role === 'guest') || (role === 'contributor') || (role === 'editor');
110
+ }
111
+ return true;
112
+ }
113
+
114
+ function canEdit() {
115
+ if (checkRoleConfig('editRole')) {
91
116
  return false;
92
- } else {
93
- return role === 'editor';
94
117
  }
95
- } else if (action === 'upload-attachment') {
96
- if ((role === 'contributor') || (role === 'editor')) {
97
- return true;
98
- } else {
118
+ if (mode === 'draft') {
119
+ return (role === 'contributor') || (role === 'editor');
120
+ }
121
+ return role === 'editor';
122
+ }
123
+
124
+ function canPublish() {
125
+ if (checkRoleConfig('publishRole')) {
99
126
  return false;
100
127
  }
101
- } else if (action === 'delete') {
102
- if (doc && !doc.lastPublishedAt) {
103
- return self.can(req, 'edit', doc);
104
- } else {
105
- return self.can(req, 'publish', doc);
128
+ return role === 'editor';
129
+ }
130
+
131
+ function canDelete() {
132
+ if (doc && (!doc.lastPublishedAt || doc.aposMode === 'draft')) {
133
+ return self.can(req, 'edit', doc, mode);
106
134
  }
107
- } else {
108
- throw self.apos.error('invalid', 'That action is not implemented');
135
+
136
+ return self.can(req, 'publish', docOrType, mode);
109
137
  }
110
138
  },
111
139
 
@@ -117,6 +145,7 @@ module.exports = {
117
145
  if (role === 'admin') {
118
146
  return {};
119
147
  }
148
+
120
149
  const restrictedViewTypes = Object.keys(self.apos.doc.managers).filter(name => ranks[self.apos.doc.getManager(name).options.viewRole] > ranks[role]);
121
150
  const restrictedEditTypes = Object.keys(self.apos.doc.managers).filter(name => ranks[self.apos.doc.getManager(name).options.editRole] > ranks[role]);
122
151
  const restrictedPublishTypes = Object.keys(self.apos.doc.managers).filter(name => ranks[self.apos.doc.getManager(name).options.publishRole] > ranks[role]);
@@ -175,7 +204,7 @@ module.exports = {
175
204
 
176
205
  return query;
177
206
  }
178
- } else if (action === 'edit') {
207
+ } else if ([ 'edit', 'create', 'delete' ].includes(action)) {
179
208
  if (role === 'contributor') {
180
209
  return {
181
210
  aposMode: {
@@ -259,7 +288,7 @@ module.exports = {
259
288
  permissions.push({
260
289
  name: 'create',
261
290
  label: 'apostrophe:create',
262
- value: self.can(req, 'edit', module.name)
291
+ value: self.can(req, 'create', module.name)
263
292
  });
264
293
  }
265
294
  permissions.push({