diff --git a/ghost/admin/app/components/posts-list/context-menu.hbs b/ghost/admin/app/components/posts-list/context-menu.hbs
index 077359982e..e607f61922 100644
--- a/ghost/admin/app/components/posts-list/context-menu.hbs
+++ b/ghost/admin/app/components/posts-list/context-menu.hbs
@@ -25,7 +25,7 @@
+
+
+
+
+
+
+
+
+
+ {{#if (eq this.post.visibility "tiers")}}
+
+
+
+
+ {{/if}}
+
+
+
+
+
diff --git a/ghost/admin/app/components/posts-list/modals/edit-posts-access.js b/ghost/admin/app/components/posts-list/modals/edit-posts-access.js
new file mode 100644
index 0000000000..80747151df
--- /dev/null
+++ b/ghost/admin/app/components/posts-list/modals/edit-posts-access.js
@@ -0,0 +1,55 @@
+import Component from '@glimmer/component';
+import {action} from '@ember/object';
+import {inject as service} from '@ember/service';
+import {task} from 'ember-concurrency';
+import {tracked} from '@glimmer/tracking';
+
+export default class EditPostsAccessModal extends Component {
+ @service store;
+
+ // We createa new post model to use the same validations as the post model
+ @tracked post = this.store.createRecord('post', {
+ visibility: 'public',
+ tiers: []
+ });
+
+ async validate() {
+ // Mark as not new
+ this.post.set('currentState.parentState.isNew', false);
+ await this.post.validate({property: 'visibility'});
+ await this.post.validate({property: 'tiers'});
+ }
+
+ @action
+ async setVisibility(segment) {
+ this.post.set('tiers', segment);
+ try {
+ await this.validate();
+ } catch (e) {
+ if (!e) {
+ // validation error
+ return;
+ }
+
+ throw e;
+ }
+ }
+
+ @task
+ *save() {
+ // First validate
+ try {
+ yield this.validate();
+ } catch (e) {
+ if (!e) {
+ // validation error
+ return;
+ }
+ throw e;
+ }
+ return yield this.args.data.confirm.perform(this.args.close, {
+ visibility: this.post.visibility,
+ tiers: this.post.tiers
+ });
+ }
+}
diff --git a/ghost/core/core/server/models/base/plugins/bulk-operations.js b/ghost/core/core/server/models/base/plugins/bulk-operations.js
index 33808c243b..644569acc6 100644
--- a/ghost/core/core/server/models/base/plugins/bulk-operations.js
+++ b/ghost/core/core/server/models/base/plugins/bulk-operations.js
@@ -39,20 +39,36 @@ function createBulkOperation(singular, multiple) {
};
}
-async function insertSingle(knex, table, record) {
- await knex(table).insert(record);
+async function insertSingle(knex, table, record, options) {
+ let k = knex(table);
+ if (options.transacting) {
+ k = k.transacting(options.transacting);
+ }
+ await k.insert(record);
}
-async function insertMultiple(knex, table, chunk) {
- await knex(table).insert(chunk);
+async function insertMultiple(knex, table, chunk, options) {
+ let k = knex(table);
+ if (options.transacting) {
+ k = k.transacting(options.transacting);
+ }
+ await k.insert(chunk);
}
async function editSingle(knex, table, id, options) {
- await knex(table).where('id', id).update(options.data);
+ let k = knex(table);
+ if (options.transacting) {
+ k = k.transacting(options.transacting);
+ }
+ await k.where('id', id).update(options.data);
}
async function editMultiple(knex, table, chunk, options) {
- await knex(table).whereIn('id', chunk).update(options.data);
+ let k = knex(table);
+ if (options.transacting) {
+ k = k.transacting(options.transacting);
+ }
+ await k.whereIn('id', chunk).update(options.data);
}
async function delSingle(knex, table, id, options) {
@@ -90,13 +106,13 @@ const del = createBulkOperation(delSingle, delMultiple);
*/
module.exports = function (Bookshelf) {
Bookshelf.Model = Bookshelf.Model.extend({}, {
- bulkAdd: function bulkAdd(data, tableName) {
+ bulkAdd: function bulkAdd(data, tableName, options = {}) {
tableName = tableName || this.prototype.tableName;
- return insert(Bookshelf.knex, tableName, data);
+ return insert(Bookshelf.knex, tableName, data, options);
},
- bulkEdit: async function bulkEdit(data, tableName, options) {
+ bulkEdit: async function bulkEdit(data, tableName, options = {}) {
tableName = tableName || this.prototype.tableName;
return await edit(Bookshelf.knex, tableName, data, options);
diff --git a/ghost/posts-service/lib/PostsService.js b/ghost/posts-service/lib/PostsService.js
index cdfd65d866..cbf780a1d3 100644
--- a/ghost/posts-service/lib/PostsService.js
+++ b/ghost/posts-service/lib/PostsService.js
@@ -2,9 +2,12 @@ const nql = require('@tryghost/nql');
const {BadRequestError} = require('@tryghost/errors');
const tpl = require('@tryghost/tpl');
const errors = require('@tryghost/errors');
+const ObjectId = require('bson-objectid').default;
const messages = {
invalidVisibilityFilter: 'Invalid visibility filter.',
+ invalidVisibility: 'Invalid visibility value.',
+ invalidTiers: 'Invalid tiers value.',
invalidEmailSegment: 'The email segment parameter doesn\'t contain a valid filter',
unsupportedBulkAction: 'Unsupported bulk action'
};
@@ -67,6 +70,23 @@ class PostsService {
if (data.action === 'unfeature') {
return await this.#updatePosts({featured: false}, {filter: options.filter});
}
+ if (data.action === 'access') {
+ if (!['public', 'members', 'paid', 'tiers'].includes(data.meta.visibility)) {
+ throw new errors.IncorrectUsageError({
+ message: tpl(messages.invalidVisibility)
+ });
+ }
+ let tiers = undefined;
+ if (data.meta.visibility === 'tiers') {
+ if (!Array.isArray(data.meta.tiers)) {
+ throw new errors.IncorrectUsageError({
+ message: tpl(messages.invalidTiers)
+ });
+ }
+ tiers = data.meta.tiers;
+ }
+ return await this.#updatePosts({visibility: data.meta.visibility, tiers}, {filter: options.filter});
+ }
throw new errors.IncorrectUsageError({
message: tpl(messages.unsupportedBulkAction)
});
@@ -146,6 +166,15 @@ class PostsService {
}
async #updatePosts(data, options) {
+ if (!options.transacting) {
+ return await this.models.Post.transaction(async (transacting) => {
+ return await this.#updatePosts(data, {
+ ...options,
+ transacting
+ });
+ });
+ }
+
const postRows = await this.models.Post.getFilteredCollectionQuery({
filter: options.filter,
status: 'all'
@@ -153,9 +182,48 @@ class PostsService {
const editIds = postRows.map(row => row.id);
- return await this.models.Post.bulkEdit(editIds, 'posts', {
- data
+ let tiers = undefined;
+ if (data.tiers) {
+ tiers = data.tiers;
+ delete data.tiers;
+ }
+
+ const result = await this.models.Post.bulkEdit(editIds, 'posts', {
+ data,
+ transacting: options.transacting,
+ throwErrors: true
});
+
+ // Update tiers
+ if (tiers) {
+ // First delete all
+ await this.models.Post.bulkDestroy(editIds, 'posts_products', {
+ column: 'post_id',
+ transacting: options.transacting,
+ throwErrors: true
+ });
+
+ // Then add again
+ const toInsert = [];
+ for (const postId of editIds) {
+ for (const [index, tier] of tiers.entries()) {
+ if (typeof tier.id === 'string') {
+ toInsert.push({
+ id: ObjectId().toHexString(),
+ post_id: postId,
+ product_id: tier.id,
+ sort_order: index
+ });
+ }
+ }
+ }
+ await this.models.Post.bulkAdd(toInsert, 'posts_products', {
+ transacting: options.transacting,
+ throwErrors: true
+ });
+ }
+
+ return result;
}
async getProductsFromVisibilityFilter(visibilityFilter) {
diff --git a/ghost/posts-service/package.json b/ghost/posts-service/package.json
index cb375e5244..cebd73671c 100644
--- a/ghost/posts-service/package.json
+++ b/ghost/posts-service/package.json
@@ -25,6 +25,7 @@
"dependencies": {
"@tryghost/errors": "1.2.24",
"@tryghost/nql": "0.11.0",
- "@tryghost/tpl": "0.1.24"
+ "@tryghost/tpl": "0.1.24",
+ "bson-objectid": "2.0.4"
}
}