Fixed collection cards not re-rendering when posts were bulk-edited

no issue

- bulk edit actions bypass the Bookshelf model hooks which meant our page reset behaviour in `onSaving` and `onDestroyed` was not being hit
- added overrides to `bulkEdit` and `bulkDestroy` to add the same page-reset behaviour any time we have a bulk edit or destroy
This commit is contained in:
Kevin Ansfield 2023-09-27 18:16:51 +01:00
parent 6e46e8b5ba
commit d541991046
2 changed files with 78 additions and 2 deletions

View File

@ -1367,6 +1367,24 @@ Post = ghostBookshelf.Model.extend({
return editPost();
},
bulkEdit: async function bulkEdit(ids, tableName, options) {
if (tableName === this.prototype.tableName) {
const result = await ghostBookshelf.Model.bulkEdit.call(this, ids, tableName, options);
if (labs.isSet('collectionsCard')) {
// reset all page HTML so collection cards can be re-rendered with updated posts
// NOTE: we can't check for only published edits here as we don't have access to previous values
// to see if a previously published post has been unpublished, so we just reset all pages
const pageResetQuery = ghostBookshelf.knex.raw('UPDATE posts set html = NULL WHERE type = "page" AND lexical IS NOT NULL');
await (options.transacting ? pageResetQuery.transacting(options.transacting) : pageResetQuery);
}
return result;
} else {
return ghostBookshelf.Model.bulkEdit.call(this, ids, tableName, options);
}
},
/**
* ### Add
* @extends ghostBookshelf.Model.add to handle returning the full object
@ -1413,6 +1431,34 @@ Post = ghostBookshelf.Model.extend({
return destroyPost();
},
bulkDestroy: async function bulkDestroy(ids, tableName, options) {
if (tableName === this.prototype.tableName) {
if (labs.isSet('collectionsCard')) {
// get count of published posts to be destroyed before they no longer exist to count
const deletedPublishedCount = await this.query((qb) => {
qb.where('type', 'post')
.where('status', 'published')
.whereIn('id', ids);
}).count({transacting: options.transacting});
const result = await ghostBookshelf.Model.bulkDestroy.call(this, ids, tableName, options);
// if we've deleted any published posts, we need to reset the html for all pages so dynamic collection
// card content can be re-rendered
if (deletedPublishedCount > 0) {
const pageResetQuery = ghostBookshelf.knex.raw('UPDATE posts set html = NULL WHERE type = "page" AND lexical IS NOT NULL');
await (options.transacting ? pageResetQuery.transacting(options.transacting) : pageResetQuery);
}
return result;
} else {
return ghostBookshelf.Model.bulkDestroy.call(this, ids, tableName, options);
}
} else {
return ghostBookshelf.Model.bulkDestroy.call(this, ids, tableName, options);
}
},
// NOTE: the `authors` extension is the parent of the post model. It also has a permissible function.
permissible: async function permissible(postModel, action, context, unsafeAttrs, loadedPermissions, hasUserPermission, hasApiKeyPermission) {
let isContributor;

View File

@ -1,4 +1,6 @@
const should = require('should');
const DomainEvents = require('@tryghost/domain-events');
const {mobiledocToLexical} = require('@tryghost/kg-converters');
const {agentProvider, fixtureManager, mockManager} = require('../../utils/e2e-framework');
const models = require('../../../core/server/models');
const assert = require('assert/strict');
@ -8,15 +10,26 @@ describe('Posts Bulk API', function () {
before(async function () {
mockManager.mockLabsEnabled('collections');
mockManager.mockLabsEnabled('collectionsCard');
agent = await agentProvider.getAdminAPIAgent();
// Note that we generate lots of fixtures here to test the bulk deletion correctly
await fixtureManager.init('posts', 'newsletters', 'members:newsletters', 'emails', 'redirects', 'clicks', 'comments', 'feedback', 'links', 'mentions');
await agent.loginAsOwner();
// convert inserted pages to lexical so we can test page.html reset/re-render
const pages = await models.Post.where('type', 'page').fetchAll();
for (const page of pages) {
const lexical = mobiledocToLexical(page.get('mobiledoc'));
await models.Base.knex.raw('UPDATE posts SET mobiledoc=NULL, lexical=? where id=?', [lexical, page.id]);
}
});
afterEach(function () {
afterEach(async function () {
// give pages some HTML back to alleviate test interdependence when pages are reset on create/update/delete
await models.Base.knex.raw('UPDATE posts SET html = "<p>Testing</p>" WHERE type = \'page\' AND html IS NULL');
mockManager.restore();
});
@ -48,6 +61,13 @@ describe('Posts Bulk API', function () {
assert.equal(response.body.bulk.meta.stats.successful, amount, `Expect all matching posts (${amount}) to be changed`);
// Check page HTML was reset to enable re-render of collection cards
// Must be done before fetch all posts otherwise they will be re-rendered
const totalPageCount = await models.Post.where({type: 'page'}).count();
const emptyPageCount = await models.Post.where({html: null, type: 'page'}).count();
should.exist(emptyPageCount);
emptyPageCount.should.equal(totalPageCount, 'no. of render-queued pages after bulk edit');
// Fetch all posts and check if they are featured
const posts = await models.Post.findAll({filter, status: 'all'});
assert.equal(posts.length, amount, `Expect all matching posts (${amount}) to be changed`);
@ -281,7 +301,7 @@ describe('Posts Bulk API', function () {
it('Can unpublish posts', async function () {
const filter = 'status:[published]';
const changedPosts = await models.Post.findPage({filter, limit: 1, status: 'all'});
const changedPosts = await models.Post.findPage({filter, status: 'published'});
const amount = changedPosts.meta.pagination.total;
assert(amount > 0, 'Expect at least one post to be affected for this test to work');
@ -301,6 +321,10 @@ describe('Posts Bulk API', function () {
// Fetch all posts and check if they are unpublished
const posts = await models.Post.findAll({filter, status: 'all'});
assert.equal(posts.length, 0, `Expect all matching posts (${amount}) to be unpublished`);
// Re-publish the posts so we don't affect later tests
const postIds = changedPosts.data.map(post => post.id);
await models.Base.knex.raw(`UPDATE posts SET status = \'published\' WHERE id IN (${postIds.map(() => '?').join(',')})`, [...postIds]);
});
});
@ -332,6 +356,12 @@ describe('Posts Bulk API', function () {
// Check if all posts were deleted
const posts = await models.Post.findPage({filter, status: 'all'});
assert.equal(posts.meta.pagination.total, 0, `Expect all matching posts (${amount}) to be deleted`);
// Check page HTML was reset to enable re-render of collection cards
const totalPageCount = await models.Post.where({type: 'page'}).count();
const emptyPageCount = await models.Post.where({html: null, type: 'page'}).count();
should.exist(emptyPageCount);
emptyPageCount.should.equal(totalPageCount, 'no. of render-queued pages after bulk delete');
});
it('Can delete all posts', async function () {